I have been working on Xamarin Forms application (this question suits WPF and UWP as well) and came across a problem that I think can be have the roots in my current project architecture.
Here is an example of application similar to my current one:
Solution MyProject
MyProject.GeoTracker
GeoLocationProvider.cs
MyProject
MainViewModel.cs
SettingsViewModel.cs
MyProject.Android
MyProject.iOS
MyProject.Shared
MyProject.GeoTracker
is referenced in MyProject
, so that MyProject
has reference of MyProject.GeoTracker
.
In SettingsViewModel.cs
I have an EventHandler:
public event EventHandler<EventArgs> SettingsChanged;
protected virtual void OnSettingsChanged() => this.SettingsChanged?.Invoke(this, new EventArgs());
Then in MainViewModel.cs:
public MainViewModel(
ISettingsViewModel settingsViewModel)
{
this.SettingsViewModel = settingsViewModel;
this.SettingsViewModel.SettingsChanged = this.OnSettingsChanged;
this.GeoLocationProvider = new GeoLocationProvider();
this.GeoLocationProvider.StartGeoTracking();
this.GeoLocationProvider.GeoLocationChanged = this.OnGeoLocationChanged;
}
This way I am able to track Settings changes in my MainViewModel.cs
.
Question: what if I want to do the same in GeoLocationProvider.cs
? It is not possible to have circle reference in solution, so I am not able to reference MyProject
in MyProject.GeoTracker
. What is possible solution?
CodePudding user response:
td;dr A circular reference can be avoided with an appropriate interface
, an event handler
, or any Dependency Injection
scheme.
INTERFACE
The principle is that the place that "needs the work done" does NOT need to know what class
performs the work. It just needs a way to say what it wants done.
I'll show an interface, rather than pick some DI library. A DI library makes details cleaner than what I do here.
- Define an
interface
inMyProject.GeoTracker
:
public interface ISomeInterface
{
void DoSomething(object obj);
}
- Declare a
static
variable to hold an implementation of that interface:
public class MyClass1
{
public ISomeInterface SomeInterface;
}
- In
MyProject
, define the implementor of that interface:
class SomeInterfaceImpl ISomeInterface
{
public void DoSomething(object obj)
{
...
}
}
- In
MyProject
, tellMyProject.GeoTracker
what implementation to use:
MyProject.GeoTracker.MyClass1.SomeInterface = new SomeInterfaceImpl();
- When
MyProject.GeoTracker
needs the work done, it does:
MyClass1.SomeInterface?.Invoke(...);
Which does nothing, if that variable has not been set.
EVENTS
An event
is often an alternative to the above. Step #2 defines an event; Step #4 does eventHandler = ...
to attach an action to perform on that event. This is a fundamental c# concept, that hopefully you have used many times, to get a standard library to execute your code, even though that library doesn't have a reference to your code. Think about that.
DEPENDENCY INJECTION
Everything else you need is elaborations on the above. Its often worth using a Dependency Injection
library to manage the details cleanly.
ADDITIONAL NOTES
Often, you'll need to define additional
interface
s, in order to pass data back and forth. If you wantMyProject.GeoTracker
to act on aMyProject.MyClass2
instance, defineinterface MyProject.GeoTracker.IMyClass2
which exposes the needed members. Thenpublic class MyClass2 : IMyClass2
.Worst case, you'll need another DLL to hold shared
interface
s. So all your projects can refer to them. Personally, I've never encountered a situation that required that.Does this require more coding than if you could directly make a circular refernce?
Yes, but IMHO its worth it: it documents places where you are forcing a "reverse-dependency": making 'lower-level' code "aware" of something in 'higher-level' code.
CodePudding user response:
When I have to summon the dark forces of circular references this is my personal technique. "Your mileage many vary." Here I have a solution with class libraries ClassLibraryA
and ClassLibraryB
. Before I am through here, they'll successfully and safely reference each other.
namespace ClassLibraryA
{
public class ClassA
{
}
}
and
namespace ClassLibraryB
{
public class ClassB
{
}
}
To bootstrap this scheme, the first Solution build is performed before they attempt to reference each other. As a matter of preference, I use a common path for both. The solution builds just fine. It has no reason to complain so far, right?.
But nothing stops me from invoking the Browse... from here:
...and so now I add the ClassLibraryA.dll to the ClassLibraryB References and vice versa in order to do this:
using ClassLibraryB;
namespace ClassLibraryA
{
public class ClassA
{
public ClassA()
{
}
public ClassB ClassB { get; set; }
}
}
and this:
using ClassLibraryA;
namespace ClassLibraryB
{
public class ClassB
{
public ClassB()
{
}
public ClassA ClassA { get; set; }
}
}
and finally this:
static void Main(string[] args)
{
var classA = new ClassA();
classA.ClassB = new ClassB();
var classB = new ClassB();
classB.ClassA = new ClassA();
Console.WriteLine($"{classA.GetType().Name} has {classA.ClassB.GetType().Name}");
Console.WriteLine($"{classB.GetType().Name} has {classB.ClassA.GetType().Name}");
}
So this is one way to thread the needle that I've used in one flavor or another. I'm not claiming this is some kind of The Answer it's just something that's worked for me.