Introduction
Separation of Concerns
In software engineering a concern is something that is responsible for a single 'side' of the functionality. I plan to give concrete coding examples of separate concerns later in this article, but for now, let us have a purely verbal discussion (without looking at the code).
Assume that you have some visual glyph on a canvas (it does not matter if it is a desktop or a web application and what software package is used). Here are some glyph related concerns listed and explained:
- The glyph carries our some Visual information - its color, size, position, visual content - this is its Visual concern.
- In most applications, glyphs also correspond to some useful data that is presented by the glyph or modified via the glyph or both. The data and the functionality to display or modify it will represent the Data concern of the glyph.
- Assume that we can remove the glyph from the canvas - the 'remove' functionality represents Removal concern of the glyph.
- Assume also that the glyph can be in selected and unselected states - its visuals change depending on whether it is selected or not. The functionality responsible for it can be factored out as a Selection concern.
To illustrate, run the MultiConcernsTest
sample application. I'll explain the code later, but now let us discuss the application features.
When you start the app, you'll see the following window:
The application lists two business groups "Astrologists" and "Alchemists" within an organization, with two people under each group.
Clicking on the top beige part containing the group name will select the group (its border will become thicker). Clicking on a person will select that person and that group. Only one group can be selected at a time and only one person can be selected within a group at a time. If you unselect a group - all its people will also get unselected.
You can use the left mouse button to open a menu containing "Remove" menu item. If you click on it, the corresponding glyph will be removed - if you click on the business group's "Remove" you'll remove the whole business group, while if you click on a person's "Remove" you will only remove that person within the business group.
Clearly for a person glyph, the visual concern is represented by the actual visual control and its functionality. The Data concern consists of the functionality containing first and last name. The Selection and Removal concerns are represented by the functionality in charge of selecting and removing the glyph.
Similar split into concerns can be done on the business group glyph level.
Note that the listed concerns are almost independent of each other - The only points of interdependence are:
- When an item is removed it should also be removed visually - WPF (which I used to implement the samples) takes care of it automatically - the View is being modified once the View Model changes.
- When an item is selected, its border should become thicker - this is achieved by some small extra XAML functionality.
In the discussion below I'll skip talking about Visual - Non-Visual concerns split. WPF and other frameworks are very good at taking care of this particular concern split (if you use MVVM pattern). I'll be talking mostly about splitting Data, Selection and Removal concerns.
The best design is to separate the concerns into separate classes/interfaces, build and test them completely separately and then put them together with little or no extra code.
Such separation of concerns has the following major advantages:
- Reuse - Selection and Removal functionality can be factored out into separate classes and reused for different glyphs and even for some non-visual code.
- Code Clarity - when data, selection and removal functionality are all mixed together they pollute the code so that it is difficult to tell where one ends and other begins.
- Code Independence - if people start placing various concerns together they will unavoidably create more dependency between various concerns. This will lead to various inter-dependency bugs when changing code corresponding to one concern might result in a bug in another.
- Testing - it is much easier and better to test all concerns separately rather than together. Understandably - there are fewer combinations if you test individual concerns rather than their Cartesian product.
Behaviors
C# behavior is a class that allows to modify the behavior of another class. Behaviors are attached to an object of a class they modify. They may update the object's properties and also add handlers to its events whenever they are attached to it.
Most people are only familiar with visual WPF behaviors, however, non-visual behaviors can also be very useful as I showed in a number of articles e.g. View-View Model based WPF and XAML Implementational Patterns. and Collection Behaviors. Behaviors are very useful when it comes to the separation of concerns since they allow to factor out a specific concern functionality into a behavior class.
Later in the article I will provide specific examples of separation of concerns using behaviors.
Roxy
Roxy is a code generator and an IoC container which I built specifically with separation of concerns in mind. The main point of this article is to demonstrate the advantage of using Roxy in easily and cleanly achieving separation of concerns.
Roxy is an open source project available at Github at Roxy.
One can use the nuget manager to download NP.Roxy and all the dlls required by it from nuget.org.
Usage and Implementation Inheritance and Smart Mixins
Before diving into samples, let us review the OOP notion of inheritance and a newer notion of mixins.
When we talk about inheritance, we essentially talk about two different (though connected) notions - Usage Inheritance and Implementation Inheritance.
In C# and Java, Usage Inheritance is represented by interfaces and by abstract or virtual super class members. C# and Java interface inheritance is as powerful as it can be. In particular, it allows the 'smart' multiple Usage Inheritance so that the properties, methods and events from several interfaces are merged into one as long as their signatures and names match.
The much spoken about "Is A" relationship is related precisely to the Usage Inheritance and has almost nothing to do with the Implementation Inheritance - indeed it is the Usage Inheritance that defines the object's usage.
The Implementation Inheritance in C# and Java is not powerful at all. Only single inheritance is allowed, and correspondingly there is no smart merging of various class member implementations.
In general, the class functionality can be demonstrated by the following picture:
The Usage is represented by the border of the shapes. The empty (white) or shaded areas represent correspondingly abstract or virtual members (properties or methods or even sets of them). Different white areas can represent the areas of different usage concerns.
Making the class work the way it should can be achieved by plugging the functionality implementations into the empty (and/or) shaded areas of the class shape (the border of the area determines its Usage interface).
This is what I think the purpose of the ideal Implementation Inheritance should be (just to fill in the blanks).
In my view the 'ideal' multiple Implementation Inheritance should provide the following features:
- It should provide the functionality to easily specify multiple Implementation 'super classes'. Several Implementation 'super classes' can even be of the same type - their functionality may be used to implement the various plugins (blank areas on the picture above).
- Instead of the 'white box' inheritance when everything of the 'super class' goes into the subclass, it should be a 'black box' inheritance under which the developer is required to specify what member of the 'super class' should be accessible in the subclass.
- The easiest way of matching the abstract or virtual 'blank' with some 'super class' functionality is by name - therfore developers should be allowed to easily rename the 'super class' members within a subclass.
Unfortunately no language natively supports this type of inheritance - C++ multiple implementation inheritance is very far from 'ideal' and consequently can lead to numerous problems and is considered to be too difficult to use.
The best place to implement such Implementation Inheritance is to make it part of new language features. Making it part of the language will ensure type safety and allow compiler optimizations. I plan to talk more about it in future articles. In the meanwhile, since we do not have such features built in, we can implement this functionality using the Wrappers/Adapters concept (see e.g. Implementing Adapter Pattern and Imitating Multiple Inheritance in C# using Roslyn based VS Extension Wrapper Generator).
In many other languages such 'wrapper' constructs are called 'mixins'. One of the main purposes of Roxy is to enable 'mixin' functionality in C#. Moreover, as I plan to demonstrate in this article and expound in subsequent articles, Roxy mixins are 'smart' in a sense that they allow to change the name of the 'super class' member within a 'subclass' and merge multiple 'super class' members into a single 'subclass' member.
Samples
Code Location
Code can be downloaded from the top of this article (NP.Concerns.Demo.zip) file, it is also available on Github: NP.Concerns.Demo
Demo Application and Brief Description of the Samples
Every sample demonstrates building the same small WPF application:
It displays an organization with 2 departments - "Astrologists" and "Alchemists" each of which contains 2 people. You can select a single department or a single person within that department (selected card has a thicker border).
When you select a person, that person's department also gets selected. When a department is unselected - all its people get also get unselected.
The image above has "Alchemists" department and "Michael Mont" within it selected.
If you right click on a department or person card, you'll get a context menu allowing you to remove the card (whether a whole department or just a person within a department).
As was mentioned above, we almost do not concern ourselves with how the visuals are implemented - we shall be concentrating primarily on the View Models.
There are 3 demo samples:
- MultiConcernsTest - provides the plainest implementation of the application. I minimize the usage of the behaviors (even though I do use some of them to shrink the code a little).
- MultiConcernsBehaviorTest - similar to the first sample, but I factor out the parent-child selection behavior into a separate class to demonstrate advantages of behavior-based separation of concerns.
- MultiConcernsRoxyTest - shows how to build the same application using Roxy. There is almost no inter-concern code - the concerns are being plugged in from separate classes and almost everything is achieved via the configuration.
MultiConcernsTest
Take a look at MultiConcernsTest.sln solution and run the a application. Make sure the application behaves as was described above: the selection, and removal work.
All of the business logic of the application is implemented via the non-visual View Model code. The View Model code is located under "ViewModels" project folder.
There are two central View Model classes: PersonVM
and BusinessGroupVM
. There is also a small class BusinessGroupsVM
which is a collection of business groups. Note that I used SingleSelectionObservableCollection
class for the collection - it provides the functionality to ensure that no more that one member of the collection is in "Selected" state. This class is defined within NP.Utilities
project referenced by our WPF application. The BusinessGroupsVM
class also contains collection behavior to facilitate removal of an item from the collection - more on it later.
The test data based on the above classes is created within MainWindow.xaml.cs class and is assigned to be the DataContext
of the main window:
public MainWindow()
{
InitializeComponent();
BusinessGroupsVM businessGroups = new BusinessGroupsVM();
BusinessGroupVM businessGroup1 = new BusinessGroupVM
{
Name = "Astrologists"
};
businessGroup1.People.Add(new PersonVM { FirstName = "Joe", LastName = "Doe" });
businessGroup1.People.Add(new PersonVM { FirstName = "Jane", LastName = "Dane" });
businessGroups.Add(businessGroup1);
BusinessGroupVM businessGroup2 = new BusinessGroupVM
{
Name = "Alchemists"
};
businessGroup2.People.Add(new PersonVM { FirstName = "Michael", LastName = "Mont" });
businessGroup2.People.Add(new PersonVM { FirstName = "Michelle", LastName = "Mitchell" });
businessGroups.Add(businessGroup2);
this.DataContext = businessGroups;
}
Take a look at the PersonVM
class. Various (non-data) related concerns are basically provided by the interfaces that the class implements: INotifyPropertyChanged
, IRemovable
and ISelectableItem<PersonVM>
.
Take a look at IRemovable
interface defined within NP.Utilities
project:
public interface IRemovable
{
event Action<iremovable> RemoveEvent;
void Remove();
}
</iremovable>
It is extremely simple - it has method Remove()
which is supposed to fire RemoveEvent
. Then a collection that the IRemovable
item belongs to can remove the item from itself.
And here is the ISelectableItem
interface:
public interface ISelectableItem<T>
where T : ISelectableItem<T>
{
bool IsSelected { get; set; }
event Action<ISelectableItem<T>> IsSelectedChanged;
void SelectItem();
}
IsSelected
property specifies whether the item is selected or not. Whenever it changes, IsSelectedChanged
event should fire. SelectItem()
method should change the IsSelected
property to true
.
Now, let us go back to the PersonVM
code - I was using C# regions to separate the concerns within the code:
public class PersonVM :
INotifyPropertyChanged,
IRemovable,
ISelectableItem<PersonVM>
{
#region Data_Concern_Region
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName => FirstName + " " + LastName;
#endregion Data_Concern_Region
#region Notifiable_Concern_Region
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion Notifiable_Concern_Region
#region Removeable_Concern_Region
public event Action<IRemovable> RemoveEvent = null;
public void Remove()
{
RemoveEvent?.Invoke(this);
}
#endregion Removeable_Concern_Region
#region Selectable_Concern_Region
public event Action<ISelectableItem<PersonVM>> IsSelectedChanged;
bool _isSelected = false;
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value)
return;
_isSelected = value;
IsSelectedChanged?.Invoke(this);
OnPropertyChanged(nameof(IsSelected));
}
}
public void ToggleSelection()
{
this.IsSelected = !this.IsSelected;
}
public void SelectItem()
{
this.IsSelected = true;
}
#endregion Selectable_Concern_Region
}
You can see that the data concern is defined within the class, while the rest of the concerns are essentially defined by the interfaces and the functionality that implements them.
Now take a look at BusinessGroupVM
class. Here is its class diagram
It has all similar concerns and similar implementations of Removable and Selectable concerns. The Notifiable concern (with PropertyChanged
event) is implemented in the super class - VMBase
- I could have also used VMBase
as a base class for PersonVM
but I wanted to show its implementation as a separate concern. Here is the BusinessGroupVM
code:
public class BusinessGroupVM : VMBase, IRemovable, ISelectableItem<BusinessGroupVM>
{
#region Data_Concern_Region
public string Name { get; set; }
public SingleSelectionObservableCollection<PersonVM> People { get; } =
new SingleSelectionObservableCollection<PersonVM>();
#endregion Data_Concern_Region
#region Removeable_Concern_Region
public event Action<IRemovable> RemoveEvent = null;
public void Remove()
{
RemoveEvent?.Invoke(this);
}
#endregion Removeable_Concern_Region
#region Selectable_Concern_Region
public event Action<ISelectableItem<BusinessGroupVM>> IsSelectedChanged;
bool _isSelected = false;
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value)
return;
_isSelected = value;
IsSelectedChanged?.Invoke(this);
OnPropertyChanged(nameof(IsSelected));
}
}
public void ToggleSelection()
{
this.IsSelected = !this.IsSelected;
}
public void SelectItem()
{
this.IsSelected = true;
}
#endregion Selectable_Concern_Region
IDisposable _behaviorsDisposable;
public BusinessGroupVM()
{
_behaviorsDisposable =
this.People.AddBehavior
(
(person) => person.RemoveEvent += Person_RemoveEvent,
(person) => person.RemoveEvent -= Person_RemoveEvent
)
.AddBehavior
(
(person) => person.IsSelectedChanged += Person_IsSelectedChanged,
(person) => person.IsSelectedChanged -= Person_IsSelectedChanged
);
this.IsSelectedChanged += BusinessGroupVM_IsSelectedChanged;
}
private void Person_RemoveEvent(IRemovable person)
{
this.People.Remove((PersonVM) person);
}
private void BusinessGroupVM_IsSelectedChanged(ISelectableItem<BusinessGroupVM> businessGroup)
{
if (!this.IsSelected)
{
foreach(PersonVM personVM in this.People)
{
personVM.IsSelected = false;
}
}
}
private void Person_IsSelectedChanged(ISelectableItem<PersonVM> person)
{
if (person.IsSelected)
this.IsSelected = true;
}
}
There are two CollectionBehavior
s defined on the class to react on removal and selecting a person:
public BusinessGroupVM()
{
_behaviorsDisposable =
this.People.AddBehavior
(
(person) => person.RemoveEvent += Person_RemoveEvent,
(person) => person.RemoveEvent -= Person_RemoveEvent
)
.AddBehavior
(
(person) => person.IsSelectedChanged += Person_IsSelectedChanged,
(person) => person.IsSelectedChanged -= Person_IsSelectedChanged
);
this.IsSelectedChanged += BusinessGroupVM_IsSelectedChanged;
}
Collection behaviors are described in my Collection Behaviors article. They allow adding event handlers on every item within an ObservableCollection
when the item is added to it and removing the corresponding event handlers when and if the item is removed.
The constructor code above, means that PersonVM
object's RemoveEvent
will have Person_RemoveEvent
handler and IsSelectedChanged
will have Person_IsSelectedChanged
handler as long as the person belongs to the People
collection of BusinessGroupVM
object.
You can see that Person_RemoveEvent
simply removes the person from the People
collection and Person_IsSelectedChanged
ensures that the BusinessGroupVM
object is selected whenever one of its PersonVM
objects gets selected.
There is also a handler for IsSelectedChanged
event of BusinessGroupVM
. It ensures that whenever the BusinessGroupVM
object is unselected, all its people are unselected also.
Now let us take a second look at BusinessGroupsVM
class:
public class BusinessGroupsVM : SingleSelectionObservableCollection<businessgroupvm>
{
IDisposable _behaviorsDisposable;
public BusinessGroupsVM()
{
_behaviorsDisposable =
this.AddBehavior
(
(businessGroup) => businessGroup.RemoveEvent += BusinessGroup_RemoveEvent,
(businessGroup) => businessGroup.RemoveEvent -= BusinessGroup_RemoveEvent
);
}
private void BusinessGroup_RemoveEvent(IRemovable businessGroupToRemove)
{
this.Remove((BusinessGroupVM)businessGroupToRemove);
}
}
</businessgroupvm>
As mentioned above, it is a SingleSelectionObservableCollection
of BusingGroupVM
objects. Such collection ensures that no more than one of the items in it is selected at a time.
It also contains functionality (implemented as a CollectionBehavior
) that ensures that the item is removed from the collection whenever the item's RemoveEvent
is fired.
Problems with the MultiConcernsTest Implementation
We find all the problems related to mixing of the concerns in our first sample.
Notice that a lot of functionality is duplicated, in particular, implementations of IRemovable
and ISelectedItem
interfaces are exactly the same within PersonVM
and BusinessGroupVM
classes. Additionally, the functionality in charge of removing a PersonVM
object from BusinessGroupVM
's People
collection is the same as the functionality for removing a BusinessGroupVM
object from BusinessGroupsVM
collection.
The second major problem is that the concerns are all mixed up - single class e.g. PersonVM
or BusinessGroupVM
contains implementations for all of their concerns and this extra code dilutes the main purpose of the class (which is to contain and present useful data).
MultiConcernsBehaviorTest
Try running MultiConcernsBehaviorTest solution - you will see that it behaves exactly the same as the previous sample.
This sample is very similar to the first one, but it has two behavior based improvements. I factored out the functionality for removing an IRemovable
from a collection into RemovableCollectionBehavior
. Also I factored out the functionality responsible for unselecting a person when its business group is selected and selecting the business group when one of its people is selected into ParentChildSelectionBehavior
.
Both behaviors are defined in NP.Utilities project under Behaviors
folder.
Here is the code for RemovableCollectionBehavior
:
public class RemovableCollectionBehavior
{
IDisposable _behaviorDisposable = null;
IEnumerable<IRemovable> _collection;
public IEnumerable<IRemovable> TheCollection
{
get => _collection;
set
{
if (ReferenceEquals(_collection, value))
return;
_collection = value;
_behaviorDisposable =
_collection?.AddBehavior
(
(item) => item.RemoveEvent += Item_RemoveEvent,
(item) => item.RemoveEvent -= Item_RemoveEvent
);
}
}
private void Item_RemoveEvent(IRemovable itemToRemove)
{
(TheCollection as IList).Remove(itemToRemove);
}
}
Using AddBehavior
extension method we assign the Item_RemoveEvent
event handler to every item within the collection. The handler removes the passed item from the collection.
Code for ParentChildSelectionBehavior
is also simple (though slightly more complex):
public class ParentChildSelectionBehavior<TParent, TChild>
where TParent : class, ISelectableItem<TParent>
where TChild : class, ISelectableItem<TChild>
{
IDisposable _childrenBehaviorDisposable = null;
TParent _parent;
public TParent Parent
{
get => _parent;
set
{
if (_parent.ObjEquals(value))
return;
if (_parent != null)
{
_parent.IsSelectedChanged -=
ParentChildSelectionBehavior_IsSelectedChanged;
}
_parent = value;
if (_parent != null)
{
_parent.IsSelectedChanged +=
ParentChildSelectionBehavior_IsSelectedChanged;
}
}
}
ObservableCollection<TChild> _children;
public ObservableCollection<TChild> Children
{
private get => _children;
set
{
if (ReferenceEquals(_children, value))
return;
_children = value;
_childrenBehaviorDisposable?.Dispose();
_childrenBehaviorDisposable = _children.AddBehavior
(
child => child.IsSelectedChanged += Child_IsSelectedChanged,
child => child.IsSelectedChanged -= Child_IsSelectedChanged
);
}
}
private void ParentChildSelectionBehavior_IsSelectedChanged(ISelectableItem<TParent> parent)
{
if (!parent.IsSelected)
{
foreach(TChild child in this.Children)
{
if (child.IsSelected)
{
child.IsSelected = false;
}
}
}
}
private void Child_IsSelectedChanged(ISelectableItem<TChild> child)
{
if ((child.IsSelected) && (this.Parent != null))
{
this.Parent.IsSelected = true;
}
}
}
Property Parent
of this class should be assigned to the parent selectable item and property Children
of this class should be assigned to a collection of ISelectedItem
children.
RemovableCollectionBehavior
is used in BusinessGroupVM
class (to remove person objects from people collection) and BusinessGroupsVM
(to remove BusinesGroupVM
objects). ParentChildSelectionBehavior
is used within BusinessgroupVM
to control selection interaction between the BusinessGroupVM
object and PersonVM
objects within its people collection People
.
You can see that the View Models code really became smaller and less confusing, e.g. the bottom of BusinessGroupVM
file now looks:
ParentChildSelectionBehavior<BusinessGroupVM, PersonVM> _parentChildSelectionBehavior =
new ParentChildSelectionBehavior<BusinessGroupVM, PersonVM>();
RemovableCollectionBehavior _removableCollectionBehavior =
new RemovableCollectionBehavior();
public BusinessGroupVM()
{
_removableCollectionBehavior.TheCollection = this.People;
_parentChildSelectionBehavior.Parent = this;
_parentChildSelectionBehavior.Children = this.People;
}
instead of more than 30 lines in the previous sample. Moreover, the reuse of the code is better since the generic behaviors can also be used in other code and even in this sample, the RemovableCollectionBehavior
is being reused in two places.
MultiConcernsRoxyTest - Roxy Implementation
Important Note
Unlike the previous samples, this demo project uses NP.Roxy nuget package from nuget.org.
Roxy Sample View Models
This is the main demo of the article - the one for which sake the article is written. It is located under MultiConcernsRoxyTest solution.
The sample essentially contains two code View Models (the rest are Roxy generated).
File PersonDataVM.cs contains IPersonDataVM
interface and PersonDataVM
class:
public interface IPersonDataVM
{
string FirstName { get; set; }
string LastName { get; set; }
string FullName { get; }
}
public class PersonDataVM : IPersonDataVM
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName => FirstName + " " + LastName;
}
The only reason we need a class - is because it has the code for FullName
property.
File BusinessGroupDataVM.cs contains IBusinessGroup
interface:
public interface IBusinessGroup
{
string Name { get; set; }
ObservableCollection<ISelectableRemovablePerson> People { get; }
}
These is all 'hand-written' View Model functionality. You can see, that it only contains data without any other concerns.
There is also file RoxyModelAssembly.cs. It contains a number of very simple interfaces and a static class RoxyModelAssembler
that assembles the full classes with the various concerns together. This file will be the focus of explanations in the rest of the section.
Brief Introduction to Roxy Functionality
In the future I plan to write more articles detailing Roxy functionality. Here, however, I only give a brief introduction to Roxy type generation in order to explain the contents of RoslynModelAssembly.cs file.
Objects of type ITypeConfig
are central to creating Roxy generated type. They contain full information about the generated type. They can be modified, until their method ConfigurationCompleted()
is called. After that they become 'Frozen' - no modification is allowed.
ITypeConfig
objects are usually obtained by calling static Roxy method Core.FindOrCreateTypeConfig<...>(string className = null)
.
The generic type arguments to this method or (various its overloads) are of primary interest to us.
Most common overload of the method is Core.FindOrCreateTypeConfig<TImplementedInterface, TSuperClass, TWrappedInterface>(string className = null)
where
TImplementedInterface
is the interface that the generated type implements. If it should not implement any interface, NP.Roxy.NoInterface
type argument should be passed. TSuperClass
is the class that the generated type will extend. If no class should be extended, NP.Roxy.NoClass
type argument should be passed. TWrappedInterface
is a special interface that defines the wrapped (mixin) objects that the generated class wraps. These objects provide the implementations for the generated class'es undefined or abstract or virtual methods and properties. In a sense these objects are the implementation inheritance 'superclasses' for the generated class.
className
argument to the Core.FindOrCreateTypeConfig<...>(string className = null)
method allows to specify the name of the generated class (should be, of course, unique within NP.Generated
namespace). If no className
is passed, Roxy
will generate a default class name from the generic type arguments passed to the method.
Once the ITypeConfig
objects are created and configured and their ConfigurationCompleted()
method is called, the objects of the generated type can be obtained by calling static method Core.GetInstanceOfGeneratedType<T>(string className = null, params object[] args);
where T
can be a base class name or implemented interface name and args
are the constructor arguments for the generated type - usually we use the default constructor and correspondingly args
array is empty.
Inteface Implementations Used for Type Configuration
I am using the generic implementations of ISelectableItem<T>
and IRemovable
interfaces located under NP.Utilities
project. The names of the corresponding classes are SelectableItem<T>
and Removable
. Here is SelectableItem<T>
code:
public class SelectableItem<T> : VMBase, ISelectableItem<T>, INotifyPropertyChanged
where T : ISelectableItem<T>
{
bool _isSelected = false;
[XmlIgnore]
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (_isSelected == value)
return;
_isSelected = value;
IsSelectedChanged?.Invoke(this);
OnPropertyChanged(nameof(IsSelected));
}
}
public event Action<ISelectableItem<T>> IsSelectedChanged;
public void SelectItem()
{
this.IsSelected = true;
}
public void ToggleSelection()
{
this.IsSelected = !this.IsSelected;
}
}
You can see that SelectableItem
also implements Notifiable concern and fires PropertyChanged
event whenever its IsSelected
property changes.
Here is Removable
code:
public class Removable : IRemovable
{
public event Action<iremovable> RemoveEvent;
public void Remove()
{
RemoveEvent?.Invoke(this);
}
}
</iremovable>
Not much of a deal.
On top of SelectableItem
and Removable
classes, I am also using the RemovableCollectionBehavior
and ParentChildSelectionBehavior
classes explained as part of the previous sample.
RoxyModelAssembler Explained
Now we are ready to explain how the ITypeConfig
objects are used within RoxyModelAssembler
static class.
Static method RoxyModelAssembler.AssembleSelectableRemovablePerson()
configures the PersonVM
type that also implements ISelectableItem
, IRemovable
and INotifiablePropertyChanged
interfaces:
public static void AssembleSelectableRemovablePerson()
{
ITypeConfig typeConfig =
Core.FindOrCreateTypeConfig<ISelectableRemovablePerson, PersonDataVM, ISelectableRemovablePersonWrapper>();
typeConfig.SetEventArgThisIdx(nameof(INotifyPropertyChanged.PropertyChanged), 0);
typeConfig.ConfigurationCompleted();
}
As you can see, it implements interface ISelectableRemovablePerson
(also defined in the same file):
public interface ISelectableRemovableItem<T> :
ISelectableItem<T>,
IRemovable,
INotifyPropertyChanged
where T : ISelectableItem<T>
{
}
public interface ISelectableRemovablePerson :
IPersonDataVM, ISelectableRemovableItem<ISelectableRemovablePerson>
{
}
(I also show the interface it extends - ISelectableRemovableItem
)
ISelectableRemovablePerson
interface inherits from another interface ISelectableRemovableItem
which combines the ISelectableItem
, IRemovable
and INotifiablePropertyChanged
interfaces. I factored ISelectableRemovableItem
interface out, because it can also be used for BusingGroupVM
functionality.
This interface also gets its data concern from IPersontDataVM
interface.
Coming back to Core.FindOrCreateTypeConfig<ISelectableRemovablePerson, PersonDataVM, ISelectableRemovablePersonWrapper>();
method, the second generic type argument to it is PersonDataVM
type, so the generated class will inherit from PersonDataVM
and its property FullName
will get the implementation from that class:
public string FullName => FirstName + " " + LastName;
The rest of the properties, events and methods of the generated type will be coming from the wrapped objects defined by ISelectableRemovablePersonWrapper
interface:
public interface ISelectableRemovableWrapper<T>
where T : ISelectableItem<T>
{
SelectableItem<T> Selectable { get; }
Removable Removable { get; }
}
public interface ISelectableRemovablePersonWrapper :
ISelectableRemovableWrapper<ISelectableRemovablePerson>
{
}
(I also show the interface it extends - ISelectableRemovableWrapper
)
This interface inherits from ISelectableRemovableWrapper
. It contains two members Selectable
of SelectableItem
type and Removable
of RemovableItem
type. These members define the implementation for the Selectable and Removable concerns. ISelectableRemovableWrapper
is factored out in order to reuse it for BusinessGroupVM
generation.
Take a look at the static method RoxyModelAssembler.AssembleSelectableRemovableBusinessGroup()
used to assemble BusinessGroupVM
class, which combines business group data concern, Selectable, Removable, Notifiable concerns as well as the RemovableCollectionBehavior
and ParentChildSelectionBehavior
:
public static void AssembleSelectableRemovableBusinessGroup()
{
ITypeConfig typeConfig =
Core.FindOrCreateTypeConfig<ISelectableRemovableBusinessGroup, ISelectableRemovableBusinessGroupWrapper>("BusinessGroupVM");
typeConfig.SetInit<SingleSelectionObservableCollection<ISelectableRemovablePerson>>(nameof(IBusinessGroup.People));
typeConfig.SetEventArgThisIdx(nameof(INotifyPropertyChanged.PropertyChanged), 0);
typeConfig.SetThisMemberMap
(
nameof(ISelectableRemovableBusinessGroupWrapper.TheParentChildSelectionBehavior),
nameof(ParentChildSelectionBehavior<ISelectableRemovableBusinessGroup, ISelectableRemovablePerson>.Parent)
);
typeConfig.SetMemberMap
(
nameof(ISelectableRemovableBusinessGroupWrapper.TheParentChildSelectionBehavior),
nameof(ParentChildSelectionBehavior<ISelectableRemovableBusinessGroup, ISelectableRemovablePerson>.Children),
nameof(IBusinessGroup.People)
);
typeConfig.SetMemberMap
(
nameof(ISelectableRemovableBusinessGroupWrapper.TheRemovableCollectionBehavior),
nameof(RemovableCollectionBehavior.TheCollection),
nameof(IBusinessGroup.People)
);
typeConfig.ConfigurationCompleted();
}
Note that a different overload of Core.FindOrCreateTypeConfig<...>(...)
method is used the one that takes only two arguments - the interface to implement and the wrapper interface.
ISelectableRemovableBusinessGroup
is the interface to implement:
public interface ISelectableRemovableBusinessGroup :
IBusinessGroup,
ISelectableRemovableItem<ISelectableRemovableBusinessGroup>
{
}
Just like ISelectableRemovablePerson
(discussed above) it inherits from ISelectableRemovableItem
which provides Selectable, Removable and Notifiable type's concerns' usage sides.
The implementation sides for these concerns are specified by interface ISelectableRemovableBusinessGroupWrapper
:
public interface IRemovableCollectionBehaviorWrapper
{
RemovableCollectionBehavior TheRemovableCollectionBehavior { get; }
}
public interface ISelectableRemovableBusinessGroupWrapper :
ISelectableRemovableWrapper<ISelectableRemovableBusinessGroup>,
IRemovableCollectionBehaviorWrapper
{
ParentChildSelectionBehavior<ISelectableRemovableBusinessGroup, ISelectableRemovablePerson> TheParentChildSelectionBehavior { get; }
}
This interface defines the ParentChildSelectionBehavior
. It also inherits from ISelectableRemovableWrapper
interface that specifies the implementations for the Selectable, Removable and Notifiable concerns (as was discussed above). Finally it also inherits from IRemovableCollectionBehaviorWrapper
that defines RemovableCollectionBehavior
(IRemovableCollectionBehaviorWrapper
interface is factored out because it is also needed for creating BusinessGroupsVM
implementation).
The rest is pretty much documented within the code:
typeConfig.SetInit<SingleSelectionObservableCollection<ISelectableRemovablePerson<<(nameof(IBusinessGroup.People));
initializes the People
collection to an empty SingleSelectionObservableCollection<ISelectableRemovablePerson>
collection within the constructor.
There are also name mappings, e.g.
typeConfig.SetThisMemberMap
(
nameof(ISelectableRemovableBusinessGroupWrapper.TheParentChildSelectionBehavior),
nameof(ParentChildSelectionBehavior<iselectableremovablebusinessgroup, iselectableremovableperson="">.Parent)
);
</iselectableremovablebusinessgroup,>
maps this
of the generated type into the ParentChildSelectionBehavior.Parent
property.
Finally the static method to generate code for BusingsGroupsVM
collection is RoxyModelAssembler.AssembleBusinessGroupsCollection()
:
public static void AssembleBusinessGroupsCollection()
{
ITypeConfig typeConfig =
Core.FindOrCreateTypeConfig<NoInterface, SingleSelectionObservableCollection<ISelectableRemovableBusinessGroup>, IRemovableCollectionBehaviorWrapper>("BusinessGroupsVM");
typeConfig.SetThisMemberMap
(
nameof(IRemovableCollectionBehaviorWrapper.TheRemovableCollectionBehavior),
nameof(RemovableCollectionBehavior.TheCollection)
);
typeConfig.ConfigurationCompleted();
}
The ITypeConfig
object does not specify implementation interface, but specifies the base class SingleSelectionObservableCollection<ISelectableRemovableBusinessGroup>
. The wrapper interface is IRemovableCollectionBehaviorWrapper
, which only contains the RemoveableCollectionBehavior
. TheCollection
property of the behavior maps into this
of the generated object.
Code for Generating the Collection of Test Objects
Code for generating the test objects (the WPF Window's DataContext
) is different now since we cannot use the constructors for generated objects but have to use Roxy functionality for that.
This code is located within MainWindow.xaml.cs file within the constructor of MainWindow
class:
public MainWindow()
{
InitializeComponent();
RoxyModelAssembler.AssembleSelectableRemovablePerson();
RoxyModelAssembler.AssembleSelectableRemovableBusinessGroup();
RoxyModelAssembler.AssembleBusinessGroupsCollection();
SingleSelectionObservableCollection<ISelectableRemovableBusinessGroup> dataContext =
Core.GetInstanceOfGeneratedType<SingleSelectionObservableCollection<ISelectableRemovableBusinessGroup>>();
this.DataContext = dataContext;
ISelectableRemovableBusinessGroup businessGroup1 =
Core.GetInstanceOfGeneratedType<ISelectableRemovableBusinessGroup>();
businessGroup1.Name = "Astrologists";
dataContext.Add(businessGroup1);
ISelectableRemovablePerson person1 = Core.GetInstanceOfGeneratedType<ISelectableRemovablePerson>();
person1.FirstName = "Joe";
person1.LastName = "Doe";
businessGroup1.People.Add(person1);
ISelectableRemovablePerson person2 = Core.GetInstanceOfGeneratedType<ISelectableRemovablePerson>();
person2.FirstName = "Jane";
person2.LastName = "Dane";
businessGroup1.People.Add(person2);
ISelectableRemovableBusinessGroup businessGroup2 =
Core.GetInstanceOfGeneratedType<ISelectableRemovableBusinessGroup>();
businessGroup2.Name = "Alchemists";
dataContext.Add(businessGroup2);
ISelectableRemovablePerson person3 = Core.GetInstanceOfGeneratedType<ISelectableRemovablePerson>();
person3.FirstName = "Michael";
person3.LastName = "Mont";
businessGroup2.People.Add(person3);
ISelectableRemovablePerson person4 = Core.GetInstanceOfGeneratedType<ISelectableRemovablePerson>();
person4.FirstName = "Michelle";
person4.LastName = "Mitchell";
businessGroup2.People.Add(person4);
}
Note that we use Core.GetInstanceOfGeneratedType<...>()
for creating the objects of generated types.
Conclusion
The main points of the article are
- Separation of concerns is essential behind building good, reusable and testable code.
- There are two types of inheritance - Usage and Implementation inheritance which losely map into C# or Java interface implementation and base class extensions.
- Every class within an application has multiple concerns to it.
- Every concern has Usage and Implementation side to it.
- Usage sides of multiple concerns can be easily merged using C# or Java language by using interface inheritance with ability to merge multiple members of the same name and signature into the same member.
- Implementation side of multiple concerns is not easy to merge by C# or Java or C++ language means alone. It is primarily to mitigate this drawback that I came up with Roxy IoC container and Code Generator.
- I advocate coming up with new language features and a special type of multiple inheritance or mixins that would improve merging of the Implementation sides of various concerns. This will allow to optimize compilation and provide better type safety features. The features of such 'multiple implementation inheritance' would be the following:
- It should be 'black box' inheritance where only specified functionality is being inherited - not the usual 'white box' inheritance where all protected and public functionality is being inherited.
- It should be easy to rename and map the 'superclass' functionality into a 'subclass' name.
- Various ways should be provided for merging multiple 'superclass' members that map into the same 'subclass' member.
- The same 'superclass' type can appear several times among the 'superclasses' of a type under different names. The members of such 'superclasses' can be mapped into different members of the 'subclass'
I plan to write more articles on Roxy describing its various features in-depth.
Acknowledgements
I would like to thank my dear wife Pazit for her help editing the article and making sure its content is clear and grammatically correct.
I have been using a great tool - Quick Diagram Tool for C# for generating the UML diagrams. My hat tip to its creator.