Introduction
In order to generalize the information dataflow between objects that communicate with specific data types, we shall present a mechanism that will enable us
to transmit the said data and modify it in a predetermined way. This purely generic black box mechanism will act as an intermediary between the data source,
the modifying components of the data, and its destination, thus providing us with a higher degree of component decoupling.
Program Dataflow
1.1 Dataflow Between Objects
We are now taking under inspection, the problem of data exchange between objects. For this particular problem, an example application will be constructed and we shall
take a look at its interactions, while its data is being modified when passed between objects. The data exchanges can be said to be performed in the following way.
The first step is where the data object is created from the object representing the source of our data. Then, the object containing the data is sent to another object,
which represents the object that will process the data, in a certain predefined way. While the last step consists of simply sending the processed data object to an object
representing the destination for our data to be used by the end user.
This process is always triggered by a certain action. Therefore this action is going to take a special position in our exposition of this problem.
1.2 Generalizing the Dataflow Concept
As explained above the data exchange can be modeled as a three step process where we get the source of the data, modify the relevant data, and set the data to its destination.
If we were to be able to generalize this process so that it can use any data type and perform any operation on this data, we would then be in possession of a generalized model
of data exchange.
We are now going to present such a model and implement it as a simple assembly which can later be used by any application. The advantage of using a generalized data exchange model
is that our triggering actions do not need to be implemented with modification objects that will modify the relevant data, in every specific case. The only thing that needs
to be done is for the developer to implement our Data Exchange Mechanism and specify the data type to use.
Since the business applications are in a lot of cases implemented to use business objects, we shall model our Data Exchange Mechanism, where the data in question is represented
by a business object. In other words, the data that is being exchanged by our Data Exchange Mechanism is a specific business object relevant for certain data exchange.
Our model can also be displayed by the following diagram, where in the first block we can see the Data Source object containing the Data object, i.e. a business object,
which gets passed to a Data processing object to get modified while in the end, the Processed data gets sent to the Data destination object.
1. Generalized model of the Data Exchange Mechanism.
The Mechanism
2.1 Examining the Mechanism
Our Data Exchange Mechanism is constructed from three interacting units, which are the following:
- Exchange Interfaces
- The Mechanism Core
- Data Modification Unit
Let us now examine all the units in detail and explain how they interact.
First, let us observe the Exchange interfaces. Since the Data Exchange Mechanism will be transferring data from its source to its destination,
we shall have to construct some interfaces that will be able to abstract the relevant data flow. The interfaces are defined as follows.
public interface IExchangeSource<T>
{
void GetSourceData(T data);
}
public interface IExchangeModify<T>
{
void ModSourceData(T data);
}
public interface IExchangeResult<T>
{
void SetSourceData(T data);
}
As we can see, we have defined three open generic interfaces, where each interface contains a single method which takes as its single parameter the open generic type
T
and returns void
.
The GetSourceData(T)
method in the IExchangeSource<T>
interface is used as an entry point for our data that is going to be processed.
An object representing the source of the data is going to implement this interface, and its containing method. Within this method, we are going to fill the the object of type
T
with the relevant data. By doing this we have managed to acquire the data to be processed.
Next, the ModSourceData(T)
method in the IExchangeModify<T>
interface is used to serve as an entry point to an object whose class definition
will implement the said interface. The object in question will then be used to modify our data of type
T
.
For the last step, we use the SetSourceData(T)
method in the
IExchangeResult<T>
interface to set the processed data on the object which
will implement this interface. Once all the relevant classes, the data source class, data modification class and the data result class have implemented the interfaces
and have used the same type for their generic type T
, we have succeeded in creating a uniform data type flow between these objects.
2.2 The Mechanism Core
Let us now turn to the Mechanism Core. In this case, the core consists of two distinct class definitions. The relevant definitions are as follows.
- The Exchange Container
- The Exchange Mechanism
The Exchange Container is defined in the following way.
class Exchange<T> where T : new()
{
public T Data = new T();
public IExchangeModify<T> ExchangeModify = new Modify<T>();
public IExchangeSource<T> ExchangeSource { get; set; }
public IExchangeResult<T> ExchangeResult { get; set; }
}
We can observe that the Exchange<T>
class is an open generic class where its generic type
T
is constrained by the
new()
keyword.
The reason for this is that this class will be instantiating an object, which our previous interfaces will have the job of being the entry points to, for various data objects.
Therefore, the Data field is the one in which we have instantiated our relevant data object of the desired type.
Next, we can see a field ExchangeModify
, that represents an object which implements the
IExchangeModify<T>
interface.
We also instantiate this field with the Modify<T>
class, which will be used to process our data.
After this, we declare two more properties, of type IExchangeSource<T>
and
IExchangeResult<T>
, which will serve as placeholders for objects
that will be the source of our data that we are going to process, and the final destination for our processed data, once we have modified it
by the ExchangeModify
object, respectively.
Next, we shall turn to the Exchange Mechanism itself. It is defined as follows.
class ExchangeMechanism<T> where T : new()
{
public ExchangeMechanism(Exchange<T> exchange)
{
exchange.ExchangeSource.GetSourceData(exchange.Data);
exchange.ExchangeModify.ModSourceData(exchange.Data);
exchange.ExchangeResult.SetSourceData(exchange.Data);
}
}
We can again observe a generic class that constrains its generic type T
with the
new()
keyword. We can also see that we have defined a constructor
that takes a single parameter of type Exchange<T>
.
It should now be noted that once this class is instantiated and gets an object of type
Exchange<T>
as its parameter, it will access all the fields
and properties that implement the interfaces defined in (1.1) and call their respective methods passing to them the Data field from
our Exchange<T>
container.
Once our Data field is instantiated to a specified type, by passing it to the
GetSourceData(T)
method, we shall fill the container
with the relevant data. After that, by passing the Data field to the
ModSourceData(T)
method, we are going to pass the field to an object
which is going to process our data in a desired way. While for the last step, after passing the Data field to the SetSourceData(T)
method,
we are simply going to pass the processed data to an object which is going to display or store the relevant data. With this construction now in place,
we have achieved the generalized notion of dataflow between any object and any data type.
2.3 Data Modification Unit
Let us now turn to the Data Modification Unit. This unit consists of two elements, which are as follows.
- Modification Element
- Dependency Injection Element
This Modification Element is represented by the Modify<T>
class, which is used as an object that is going to call another object to process our data.
We can see it defined in the following manner.
class Modify<T> : IExchangeModify<T>
{
public void ModSourceData(T data)
{
new Activate<T>(data);
}
}
The Modify<T>
class implements the IExchangeModify<T>
interface ensuring the uniform dataflow between the data source and that destination
of the processed data. We can see that in the ModSource(T)
method, we have instantiated an object of type
Activate<T>
.
This object is going to instantiate a specific object depending on the type of our generic type
T
, and pass to it the data parameter to modify it.
This task is achieved by using the mechanism of Dependency Injection Element[1]. To explain the workings of the element, let us first observe
the Activate<T>
class, which is defined as follows.
class Activate<T>
{
public Activate(T c)
{
Activator.CreateInstance(new TypeOf<T>().Type, c);
}
}
The Activate<T>
class will facilitate us with the required Dependency Injection abilities we need. It simply defines a constructor which takes a generic parameter
of type T
, and passes it to an object that is going to be instantiated by the
Activator
class. Thus the object in question can simply get the access to,
and modify our data. The type of the object instantiated is determined and contained by the
Typeof<T>
class and its Type
property.
Let us now turn our attention to the TypeOf<T>
class. We can see it defined as follows.
class TypeOf<T> : XmlConfiguration<T>
{
public Type Type { get; set; }
public TypeOf()
{
Type = Assembly.LoadFrom(XmlConfig.Element("Assembly").Value)
.GetType(XmlConfig.Element("Type").Value);
}
}
The TypeOf<T>
class has a single property Type
which gets its value set in the constructor.
The Type
property is set with an instance of the type Type
, in order for our
Activate<T>
class to instantiate it.
This is done by loading a specified assembly and getting the type of the class we wish to use to modify the data. This is achieved by accessing an XML file
and getting the values from the elements Assembly
and
Type
, which represent the assembly and the type respectively, where the relevant class is defined.
This is achieved by the XmlConfiguration<T>
class from which our
TypeOf<T>
class derives from.
We can see it defined in the following way.
class XmlConfiguration<T>
{
protected XElement XmlConfig =
XDocument.Load(typeof(T).FullName.Replace(".", @"\") + ".xml").Root;
}
The XmlConfiguration<T>
class simply defines a field of type
XElement
which will hold an XML file with the names of the relevant assembly
and the type to instantiate. We can see that this is a generic class, and the reason for this is that its generic type
T
is the same generic type that
we have declared in our interfaces defined in (1.1).
In this way, we have achieved the prerequisite that our Data Exchange Mechanism is specific to the type of the data we are going to modify.
To understand this, let us explain how the XmlConfig
field is instantiated. Once we have decided which type our generic type
T
is going
to be within our application, this information will reach the XmlConfiguration<T>
class. The
XmlConfig
field is then filled in the following way.
We use the XDocument
class, and invoke its Load(string)
method. By using the value in the
FullName
property gained
by calling the typeof(T)
keyword, we have effectively produced the namespace and the class name from our data's type. Which is of the form:
Namespace1.Namespace2. ... .NamespaceN.ClassName (1.8)
Then, by using the Replace(string, string)
method on our newly acquired information and using the string "." and @"\" as the parameters,
we have now transformed the namespace and the class name of our data's type, into a relative path of the form:
Namespace1\Namespace2\ ... \NamespaceN\ClassName (1.9)
In other words, from the namespace and class name definitions, we have produced the names of folders in a relative path. At the end, when we add the string
".xml" to the string containing the relative path, we have transformed the last folder name, into an XML filename, of the following form:
Namespace1\Namespace2\NamespaceN\ ... \ClassName.xml (2.0)
In other words, based on our generic type's full name, we have constructed a relative path to an XML file which is contained in a folder structure that corresponds
to the type's namespace structure, while the filename of the XML file corresponds to the type's class name.
Thus when this string is loaded into the Load(string)
method, the method will load the XML file corresponding to the name of our generic type
T
and whose relative path starts at our main application's executing folder.
With this mechanism now in place, it is easy to load any XML file, depending on the type we determine to use. Also, a clean folder structure is enforced,
for storing the relevant XML files, corresponding to the namespaces of our generic type's class.
Once the data is loaded, the XML of the following structure is read.
// (2.1)
="1.0" ="utf-8"
<root>
<Assembly>FolderName\Assembly.dll</Assembly>
<Type>Namespace.SubNamespace.ClassName</Type>
</root>
It should now be easy to see that once the Assembly
and the
Type
elements are read, we will get the names of the assembly we want to load,
and the name of the class that we wish to instantiate. When this information is sent to the
LoadFrom(string)
and GetType(string)
methods in the definition (1.6),
we are going to be in the possession of the type that we want our data, of generic type
T
, to be modified with.
It goes without saying that the modifying class in question should have as its single parameter the generic type
T
, we defined previously. Then, the definition (1.5)
simply instantiates this class and passes to its constructor, the data we wish to modify.
With this mechanism now in place, we have achieved the second prerequisite of being able to modify our data, in any way. Since it will be possible to simply create libraries
and types that will be used to process the aforementioned data, and by using the corresponding XML file, we can determine which class exactly do we want to use to modify our data.
Since this form of Dependency Injection is done by the use of Reflection[2], we can decide which class shall be used even after we have compiled the main application.
With both prerequisite 1 and prerequisite 2 now achieved, we have successfully constructed the generalized model of the Data Exchange Mechanism.
It will now suffice for us to simply wrap the ExchangeMechanism<T>
class defined in (1.3) so it can be used properly. That definition is the following.
public class CommitExchange<T> where T : new()
{
public CommitExchange(IExchangeSource<T> source, IExchangeResult<T> result)
{
new ExchangeMechanism<T>(new Exchange<T>() { ExchangeSource = source,
ExchangeResult = result });
}
}
The CommitExchange<T>
class takes two parameters of type
IExchangeSource<T>
and IExchangeResult<T>
in which the objects
containing the source data and the destination of processed data will be passed to, respectively. Then the constructor simply loads those object to the properties
of the Exchange<T>
object and passes it to our ExchangeMechanism<T>
object.
Once the object is passed to the mechanism the data exchange shall occur and our initial source data will be modified, and then stored to the object that
is determined to hold the processed data.
With this final definition, we have finished presenting the Data Exchange Mechanism. We shall now turn
to the exposition of our example application, that is, how we are supposed to implement the mechanism.
Implementing the Mechanism
3.1 A Simple Example
We are now going to produce an example of how the Data Exchange Mechanism is implemented. Also, we shall show one of the possible optimal implementations.
Our example is going to be a simple WPF application concerned with calculating both Simple Interest and Semi-Annually Compounded Interest.
The solution consists of four projects which are the following:
- TestExchange
- Data Exchange
- Interest
- Types
The architecture of the solution is set up in the following way.
2. The application project structure.
As we can clearly see, our main application containing the WPF project is referencing the Data Exchange project that is going to provide us with the Data Exchange Mechanism.
This strong reference is indicated by a solid line. Our main project also references the Types project where the types that are going to be modified are defined, that is, our business objects.
We can also see the Interest project which contains the classes which will be used to calculate the interest, in other words, to modify our data. This project
also references the Types project.
It should be noted that our main project does not reference the Interest project because the Interest assembly will be loaded by the Data Exchange Mechanism at runtime,
thus we are free to later on add or remove the assemblies used for modifying our data as the need for them arises. The fact that this assembly is not strongly referenced
by the Data Exchange Project, is indicated by a dotted line.
Let us now examine our projects briefly.
3.2 Types
The Types assembly is the simplest of our assemblies and it only contains the definitions for types that will represent our data. In our case, what we want to do is calculate
the Simple and Compounded Interest for some initial investment. Thus, our class definitions are as follows.
public class SimpleRate
{
public double Investment { get; set; }
public double Rate { get; set; }
public double Time { get; set; }
public double Payment { get; set; }
}
public class CompoundedRate
{
public double Investment { get; set; }
public double Compounding { get; set; }
public double Rate { get; set; }
public double Time { get; set; }
public double Payment { get; set; }
}
As can easily be seen, both the SimpleRate
and CompoundedRate
classes that represent our compounding conventions have the same properties with
the CompoundedRate
also having the Compounding
property which will represent the amount of times we compound our interest in a year.
The properties that are contained in both classes represent the following values. The
Investment
property represents our initial monetary investment,
the Rate
property represents the rate at which the investment will be calculated, the
Time
property represents the number of years our investment
will be held, while the Payment
property represents our final payment, that is, our earned interest.
We can also see that this project is referenced by both the TestExchange project and the Interest project. The reason for this is that our Data Exchange Mechanism is going
to send the data from the main TestExchange project to the Interest project to be modified, while retaining for us the strongly typed data,
and thus allowing us to have IntelliSense at our disposal.
3.3 Interest
The Interest project is the one that represents our modifying project. We are going to use the types defined in this project to modify our data.
Since we are going to calculate both Simple and Compounded interest, we will need to construct at least two types to perform the job.
We shall start constructing our calculation mechanism by defining the IConvention<T>
interface in the following way.
interface IConvention<T>
{
void GetInterest(T rate);
}
This interface is used to identify the compounding convention used. In our case, we have the Simple Interest and the Semi-Annually Compounded Interest.
Thus, we can say that in the first case we have no compounding while in the second case we have the semi-annual compounding.
The generic type T
shall represent the convention. The types from the definition (2.3) are used to represent the conventions. We can also observe that
our interface has a single method named GetInterest(T)
, which will be used to calculate the interest itself.
Let us now define the class that will be used for calling the GetInterest(T)
method.
class Calculate<T>
{
public Calculate(IConvention<T> convention, T rate)
{
convention.GetInterest(rate);
}
}
The generic Calculate<T>
class simply defines a constructor that takes
two parameters. The first parameter is of type IConvention<T>
to which we shall
pass an object implementing the IConvention<T>
interface, and in its
GetInterest(T)
method, containing the operations to perform the calculations.
The second parameter is the generic type T
parameter which represents the compounding convention. This is the object containing all the relevant data
for our object to calculate the interest.
The next two definitions are the classes that will implement the
IConvention<T>
interface.
class Simple : IConvention<SimpleRate>
{
public void GetInterest(SimpleRate rate)
{
rate.Payment = rate.Investment * (rate.Rate / 100.0) * rate.Time;
}
}
class Compounded : IConvention<CompoundedRate>
{
public void GetInterest(CompoundedRate rate)
{
double temp = 1 + ((rate.Rate / 100.0) / rate.Compounding);
temp = Math.Pow(temp, (rate.Compounding * rate.Time));
rate.Payment = rate.Investment * temp;
}
}
The operations defined in these two classes are not particularly important. It suffices for us to say that the classes are simply calculating the simple and the compounded interest.
What is important is that both classes implement the IConvention<T>
interface. While each class implements either the
SimpleRate
or the CompoundedRate
for their generic types, depending on whether they are used to calculate the Simple or Compounded Interest.
For our last two class definition, we are going to show the classes that are exposed outside the assembly and are going to be invoked by our Data Exchange Mechanism.
public class SimpleInterest
{
public SimpleInterest(SimpleRate rate)
{
new Calculate<SimpleRate>(new Simple(), rate);
}
}
public class CompoundedInterest
{
public CompoundedInterest(CompoundedRate rate)
{
new Calculate<CompoundedRate>(new Compounded(), rate);
}
}
Both class definitions are similar, and differ in only having the different type of the single parameter in their constructors. The
SimpleInterest
class
takes the SimpleRate
type as its parameter, then it instantiates the
Calculate<T>
class, with the SimpleRate
as its generic type
and passes to its first parameter the object of type Simple
, which will calculate the Simple Interest while for its second parameter,
it passes to it the object that contains the relevant data to use for the calculation.
It should be easy to see, that the CompoundedInterest
class does the same for us to calculate the Semi-Annually Compounded rate.
3.4 – TestExchange
The TestExchange project is our main project which is the entry point to our example application. Without going into a detailed description,
we shall briefly explain the key features of the project.
Our main window is designed in the following way.
3. The application GUI
We can see its interface is formed so that we can enter the relevant values in the
TextBox
controls, invoking the relevant interest-specific calculations
by the Button
controls, and that we can also see the calculated values on the bottom
Label
control.
These three TextBox
controls are actually contained in a complex custom control used to enter the data into our application.
Therefore, this control is going to represent our object which will serve as our data source. The bottom
Label
control is a simple custom control
that is going to be used to show the processed data, therefore this is the object used by our Data Exchange Mechanism to display the processed data.
While the classes in the Interest project are used as the modifying classes by our Data Exchange Mechanism.
Let us now observe the method for constructing the complex custom control that represents our data source. We shall start with the following class definition.
abstract class AbstractControl<T> : DockPanel where T : new()
{
protected T Controls = new T();
public AbstractControl()
{
new AddControlsToGrid(this);
}
}
This abstract class definition derives from the DockPanel
class and constrains its generic
T
type with the keyword new()
.
This is done so that we can automatically create an instance of the generic type
T
using the Controls
field. This is where our controls,
that are going to be displayed by our custom control, are going to be held.
Furthermore, the constructor simply instantiates the AddControlsToGrid
class and passes to it the
this
keyword, representing the object
of the current class itself. This is done so that the instantiated object can add all the relevant child controls to our complex control which will
display all the child controls on the main window. This is done in the following way.
class AddControlsToGrid
{
public AddControlsToGrid(DockPanel dockPanel)
{
var Grid = new Grid();
new AddChildControls(dockPanel, Grid);
new AddGridToMainControl(dockPanel, Grid);
}
}
We can observe that our AddControlsToGrid
class contains a single variable of type
Grid
which will hold all of the controls that we wish
to have on our main custom control. The constructor takes a single parameter of type
DockPanel
, representing our main custom control.
In order to now add all the relevant controls to the Grid
object and then to the main custom control, we first instantiate an object
of type AddChildControls
and then the AddGridToMainWindowControl
object, and pass to them a reference of our main custom control,
and then the Grid
object itself.
Let us now observe the definition of the AddChildControls
class as follows:
class AddChildControls
{
public AddChildControls(DockPanel dockPanel, Grid grid)
{
var control = new GetControl(dockPanel).Control;
control.GetType()
.GetFields()
.Where(x => x.FieldType.ImplementsInterface<IChild>())
.ToList()
.ForEach(x => grid.Children.Add((UIElement)x.GetValue(control)));
}
}
We first need to get the field from our main custom control that holds all the relevant child controls.
This is done by the GetControl
class. Once this is done, we shall simply get the type of the field we just selected, get its type and its fields if they implement
the IChild
interface. This way we shall distinguish the controls that need to be displayed on the custom control, from other helper fields.
Once we get all the relevant fields, we create a list from those fields, and by invoking the
ForEach(Action<T>)
method, we add every field to the Grid object.
Let us now look at how the GetControl
class has performed its part.
class GetControl
{
public object Control { get; set; }
public GetControl(DockPanel dockPanel)
{
Control = dockPanel.GetType()
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
.Where(x => x.FieldType.ImplementsInterface<IControl>())
.First()
.GetValue(dockPanel);
}
}
Once an instance of our custom control is passed to the constructor, we get the control's type, then get its protected fields only if they implement the
IControl
interface.
This way we shall distinguish the field that contains our controls that need to be displayed on the custom control, from other fields. And then we get the instance from the
custom control we passed as the parameter.
For the last step, we shall observe the definition of the
AddGridToMainControl
class.
class AddGridToMainControl
{
public AddGridToMainControl(DockPanel dockPanel, Grid grid)
{
var children = dockPanel.GetType()
.GetProperty("Children")
.GetValue(dockPanel, null);
children.GetType()
.GetMethod("Add")
.Invoke(children, new UIElement[] { grid });
}
}
Once the AddGridToMainControl
class is instantiated, we use it to get the Children
property from our custom control and invoke its Add(UIElement)
method, passing to it the grid
parameter which represents the object of type Grid
, which we have previously loaded with all the relevant child controls.
This is done so we can add this object to our main custom control as its child, so that it can display all the relevant child controls on the application's main window.
We shall now continue to our concrete implementation of our custom control that is going to be displaying controls on the application's main window.
class InterestControl : AbstractControl<Interest>,
IExchangeSource<SimpleRate>, IExchangeSource<CompoundedRate>
{
public void GetSourceData(SimpleRate data)
{
if (new TestForValue().Within(Controls.InvestmentTextBox, Controls.RateTextBox, Controls.TimeTextBox))
{
data.Investment = Controls.InvestmentTextBox.GetData().ToDouble();
data.Rate = Controls.RateTextBox.GetData().ToDouble();
data.Time = Controls.TimeTextBox.GetData().ToDouble();
}
}
public void GetSourceData(CompoundedRate data)
{
if (new TestForValue().Within(Controls.InvestmentTextBox, Controls.RateTextBox, Controls.TimeTextBox))
{
data.Investment = Controls.InvestmentTextBox.GetData().ToDouble();
data.Rate = Controls.RateTextBox.GetData().ToDouble();
data.Time = Controls.TimeTextBox.GetData().ToDouble();
data.Compounding = 2.0;
}
}
}
We can now see that our InterestControl
derives from the AbstractControl<T>
class, and uses the type
Interest
for its generic T
type.
Needless to say, this will automatically have an effect of instantiating a class of type
Interest
in our derived field Controls
.
Furthermore, all the controls defined in the Interest
class will automatically be added to our
InterestControl
.
Let us now turn to the central point of our example. The implementation of the
IExchangeSource<T>
interface for our Data Exchange Mechanism.
We can instantly notice that we have actually used two IExchangeSource<T>
interfaces, and not just one. This will demonstrate the fact that
we are able to use any number of types with a single control, and the Data Exchange Mechanism will be able to handle them all in the same way.
Since we are interested in calculating both the Simple and the Compounded Interest rate, our first interface is defined with the
SimpleRate
type,
while the second one is defined with the CompoundedRate
type.
In order to implement those interfaces we define two GetSourceData(T)
methods,
implemented with SimpleRate
and CompoundedRate
types accordingly. These methods will get the data that we will enter in the
TextBox
controls on our main window.
Both methods act as a way of telling the Data Exchange Mechanism that this is how the custom control communicates with the mechanism and sends it the relevant data.
Both methods validate the data and fill the appropriate values that will be sent back to the Data Exchange Mechanism to be processed.
The only difference is that the second method also has the Compounding
property which will represent the Semi-annually compounded rate,
and therefore, we set this property to the value of 2.0. Thus we have now successfully implemented the first part of our Data Exchange Mechanism.
The only relevant point left to explain is the Interest
class which is going to serve as a container for our controls displayed by the
InterestControl
class
on our main window. The class is simply defined in the following way.
class Interest : IControl
{
public IChild InvestmentTextBox = new InvestmentTextBox();
public IChild RateTextBox = new RateTextBox();
public IChild TimeTextBox = new TimeTextBox();
public IChild InvestmentLabel = new InvestmentLabel();
public IChild RateLabel = new RateLabel();
public IChild TimeLabel = new TimeLabel();
}
The class derives from the IControl
interface which is an empty interface, simply in order to be distinguished in the definition (3.1).
It also contains the instantiated fields of type IChild
, that are going to be serving as elements on our main window. While the
IChild
interface
is also used in the definition (3.0) to distinguish our controls from other helper fields, it also contains the
GetData()
method which is used
for validation in the definition (3.3) by the Within(params IChild[] children)
method. This method simply tests for the existence of values
within our TextBox
controls and is not currently important to us.
public interface IChild
{
string GetData();
}
Let us now turn to the second step in the process of implementing the Data Exchange Mechanism. We shall now implement the
IExchangeResult<T>
interface
within a Label
control, in order to display the relevant data.
class PaymentLabel : AbstractLabel, IExchangeResult<SimpleRate>, IExchangeResult<CompoundedRate>
{
public PaymentLabel()
{
Content = "Value";
VerticalContentAlignment = VerticalAlignment.Top;
Height = 124;
Width = 698;
}
public void SetSourceData(SimpleRate data)
{
Content = data.Payment.ToString("C");
}
public void SetSourceData(CompoundedRate data)
{
Content = data.Payment.ToString("C");
}
}
Aside from the AbstractLabel
class which is unimportant to the discussion of how to implement the Data Exchange Mechanism, our
Payment
Label
control
implements the IExchangeResult<T>
interface in two ways. One implementation uses the
SimpleRate
type, while the other one uses the CompoundedRate
.
Both of these interfaces are implemented by defining a SetSourceData(T)
method which will serve for setting the relevant data to the
Content
property
of our Payment
Label
control. We can observe that we implemented both instances to display the value as a currency by using the
ToString(string)
method.
It should be easy to see now that once our data is processed, it will be displayed by this
Label
control exactly as it is specified by these two methods.
For our lest step, we shall deal with entering the relevant data, processing it, and displaying it to the end user. We should mention that we do not need
to implement the IExchangeModify<T>
interface because this interface is, as it has been shown in definition (1.4) already implemented
by the Modify<T>
class that is contained in the Data Exchange Mechanism itself.
The only thing left now to do is to set the relevant XML file to point to a certain assembly which will contain the class definition that can process our data,
that is, that can calculate Simple and Compounded Interest. Those classes are defined in the definition (2.6). What is left to do is to place the assembly
containing these class definitions in a folder to which our XML file will point to.
Let us now turn to implementing the last step, which will calculate the Simple interest rate.
Since our business object for transferring the Simple Interest rate data is contained in the class named
SimpleInterest
, which is located in the namespace Types.Interest
,
it is only natural to place our XML configuration file in the corresponding directory structure, that is, Types\Rate, within our main executing directory.
We should also name the XML file as SimpleInterest.xml.
The Data Exchange Mechanism will now look for our XML file in this exact place and look
for this exact filename. Once the assembly and the XML configuration file are in place, we can set the XML file as follows.
// (3.7)
="1.0" ="utf-8"
<root>
<Assembly>Components\Interest.dll</Assembly>
<Type>Interest.CalculateInterest.SimpleInterest</Type>
</root>
From this definition, we can notice that we have placed the assembly that contains the relevant definition that will calculate the Simple Interest rate,
namely the Interest.dll in the Components folder within our main executing directory.
Also, we have pointed the Data Exchange Mechanism to load the SimpleInterest
class, defined in the
Interest.CalculateInterest
namespace within
the Interest.dll assembly.
Without the loss of generality, we shall only mention that the same process is performed for calculating the Compounded interest,
with the only difference being the the names of the XML configuration file, and the class that will calculate the Compounded Interest,
which are named CompoundedInterest
in both cases.
Before calculating our interest, we are now only left with implementing the
Button
control which will trigger our Data Exchange Mechanism
to perform its job. So let us now try and explain how we can construct a Button
control, which can be used optimally with the Data Exchange mechanism.
First, we start by defining the AbstractButton
class in the following way.
abstract class AbstractButton : Button, IChild
{
public AbstractButton()
{
HorizontalAlignment = HorizontalAlignment.Center;
Height = 25;
Width = 250;
}
public void SetData<T>(IExchangeSource<T> source, IExchangeResult<T> result) where T : new()
{
Click += new RoutedEventHandler((object o, RoutedEventArgs e) => new CommitExchange<T>(source, result));
}
public string GetData()
{
return "";
}
}
While observing the current definition, aside from the constructor defining the display-specific properties, and the
IChild
interface with its validation
method we can notice that our abstract class derives from the Button
class.
These definitions are currently not important for the implementation of the Data Exchange Mechanism. The only important definition
is the SetData<T>(IExchangeSource<T>, IExchangeResult<T>)
method. This method shall be used to set our
Button
control in order to use the Data Exchange Mechanism.
This is achieved in the following way. We can notice that the generic type
T
derives from the new()
keyword.
This is needed in order for our Data Exchange Mechanism to create an instance of our business object we are processing.
Next, we can see that once we pass the objects to both parameters of our method, one representing the source of our data, and the other representing the destination,
they will both be placed as arguments for the instance of the CommitExchange<T>
type object.
The generic type T
represents a certain business object, for which the source object is going to serve the data, and the result object is going to serve
as its destination, once the data is processed.
Furthermore this single line of code in our method is constructed in such a way, so as to represent
an anonymous method placed as a parameter for an EventHandler
of type
RoutedEventHandler
.
Clearly, this object is added to the Button
controls' Click
event.
Thus once this method is invoked with a certain generic type definition and two controls, one serving as the source of data and another providing the destination for the data,
the Button
control will, once it is clicked initiate the Data Exchange Mechanism, for this particular generic type.
We should now simply make a concrete Button
control which will implement the
AbstractButton
class in the following way.
class SimpleButton : AbstractButton
{
public SimpleButton()
{
Content = "Simple Interest";
}
}
This is a sufficient definition to represent the Button
control that will be used to trigger the Data Exchange Mechanism in order to calculate the Simple Interest.
Without the loss of generality, the same is done for the Compounded Interest.
We are now only left with initiating our Button
controls
to actually use the Data Exchange Mechanism for processing the data. For this we have the following definition.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SimpleButton.SetData<SimpleRate>(CustomInterest, Payment);
CompoundingButton.SetData<CompoundedRate>(CustomInterest, Payment);
}
}
We can now see our MainWindow
definition that our application will display. The only two method invocations we need to execute, are calls
to the SetData<T>(IExchangeSource<T>, IExchangeResult<T>)
method on each
Button
control. Setting each Button
control
to its respective business object type, will let use invoke the Data Exchange Mechanism to calculate the Simple and Compounded interest rate accordingly.
Furthermore, by placing our CustomInterest
object, defined in (3.3) and
Payment
object defined in (3.6)
as parameters to our method, acting as the source of our data and the destination respectively, we have set up our
Button
controls to use these
two objects as parameters for our Data Exchange Mechanism, once the Button
control is clicked.
We should notice that the advantage of this implementation is that we have defined a
Button
control in one place, but we have left the actual implementation
to the very last moment. It should be obvious that the SimpleButton
, if invoked again, with the
CompoundedRate
for its generic type T
would be able to serve as the trigger for calculating the Compounded Interest rate also.
And so with this last part of our mechanism now in place, we have successfully created and demonstrated a generalized type of data exchange, for business objects,
using our Data Exchange Mechanism.
Alternative Implementations
4.1 Button Controls
Let us now look at some examples of alternative implementations of the Data Exchange Mechanism.
We shall start out with the Button
controls. It is possible to implement our
Button
controls in the following way.
abstract class AbstractButton<T> : Button, IChild where T : new()
{
public AbstractButton()
{
HorizontalAlignment = HorizontalAlignment.Center;
Height = 25;
Width = 250;
}
public void SetData(IExchangeSource<T> source, IExchangeResult<T> result)
{
Click += new RoutedEventHandler((object o, RoutedEventArgs e) =>
new CommitExchange<T>(source, result));
}
public string GetData()
{
return "";
}
}
It is possible to define our AbstractButton
class as a generic
AbstractButton<T>
class, where its generic T
type derives from
the new()
keyword. We also remove the generic definition from the SetData<T>(IExchangeSource<T>, IExchangeResult<T>)
method,
and make it a non-generic one.
class SimpleButton : AbstractButton<SimpleRate>
{
public SimpleButton()
{
Content = "Simple Interest";
}
}
We now implement our SimpleButton
control by making it derive from the AbstractButton<T>
class and implementing its generic
T
type with the SimpleRate
class. In this way, we have concretely defined our SimpleButton
class to only trigger data exchanges where the SimpleRate
type is used for our business object.
It is obvious that this implementation is less flexible. Since we can not use any other type with this particular
Button
control, unless it derives
from the SimpleRate
class. But now we do not need to specify which type to use when we are setting the source and the destination of our data. We can see
this in the following example.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SimpleButton.SetData(CustomInterest, Payment);
CompoundingButton.SetData(CustomInterest, Payment);
}
}
4.2 – XML Configuration
The XmlConfiguration<T>
class for loading the type-specific XML configuration file was shown in definition (1.7).
It has been shown that it loads the XML files, from a relative path, starting from our main application's executing directory.
It is possible to implement this class in another way so as to specify where exactly in our executing directory we wish to locate our XML files.
So let us observe the following definition.
class XmlConfiguration<T>
{
protected XElement XmlConfig { get; set; }
public XmlConfiguration()
{
string path = XDocument.Load(@"Configuration\Configuration.xml")
.Root
.Element("Path")
.Value;
XmlConfig = XDocument.Load(path + typeof(T).FullName.Replace(".", @"\") + ".xml").Root;
}
}
Our XmlConfig
field is now transformed into a property which will be loaded in the constructor. If we want to put our XML folder structure
in a specific starting folder we can continue our implementation in the following way.
We could create a folder named Configuration in our main application's executing directory, and create an XML file named Configuration.xml
in the said folder. Now we can load this XML file and get the starting path we are going to use for our XML folder structure from its
Path
element.
This can be done, if we define the XML file in the following way.
// (4.5)
="1.0" ="utf-8"
<root>
<Path>Configuration\</Path>
</root>
If we were to now place our Types\Rate folder structure inside the Configuration folder, our Data Exchange Mechanism would load the appropriate file,
starting from the new location specified in the definition (3.8).
References
- [1] Mark Seemann: Dependency Injection in .NET; Manning Publications, 1 edition (September 28, 2011)
- [2] Joseph Albahari, Ben Albahari: C# 4.0 in a Nutshell: The Definitive Reference; O'Reilly Media, Fourth Edition edition (February 10, 2010)