Introduction
The article AOP Container introduced an AOP model that enables any IoC container to have AOP capabilities
by configuration. As examples, the model was applied to MEF, Unity,
and Windsor Containers. In this article, I discuss how the AOP Container together with Unity Container
is used to add AOP capabilities to a WPF application.
Background
It is quite easy to add aspects to business objects using an AOP Container. You define aspect methods, configure them in the application configuration file to associate them
with business components, and use one of the customized AOP Containers to instantiate your business objects. The AOP Container attaches aspects to objects automatically
based on the configuration settings.
WPF (Windows Presentation Foundation) provides some very powerful features to improve UI development experience, i.e., data binding between data sources
and user controls, event processing between user actions and data sources. This powerfulness comes with dependencies and limits. In this article, I discuss
how the AOP Container is used to add AOP capabilities to a WPF application and how to address issues between WPF and the business object model based on the AOP Container.
Problem
In this article, we try to build a rather complicated application to demonstrate component design, aspect design, and configuration, and address issues related to WPF application.
As functional (business) requirements, this application displays employees based on the selection of a department. A user can also modify existing employees, add new employees,
or delete existing employees.
In addition to the above functional requirements, we also like the application to meet non-functional (system) requirements like logging and security checking. We also
like to have some flexibility to sort employees by different criteria from a business point of view, e.g., sorting by employee's last name, by employee's birth day, etc.
The application development involves common tasks like business component design, aspect design and configuration, and tasks specific to WPF application like data binding
and change notifications, etc.
Common Tasks
Common tasks are independent of an application type. They include business component design, aspect design, and configuration.
Business Components
The two business components, Employee
and Department
, are defined as follows. They are simple classes implementing the interfaces IEmployee
and IDepartment
, respectively.
public interface IEmployee
{
System.Int32? EmployeeID { get; set; }
System.String FirstName { get; set; }
System.String LastName { get; set; }
System.DateTime DateOfBirth { get; set; }
System.Int32? DepartmentID { get; set; }
System.String DetailsByLevel(int iLevel);
}
public class Employee : IEmployee
{
#region Properties
public System.Int32? EmployeeID { get; set; }
public System.String FirstName { get; set; }
public System.String LastName { get; set; }
public System.DateTime DateOfBirth { get; set; }
public System.Int32? DepartmentID { get; set; }
#endregion
public Employee(
System.Int32? employeeid
, System.String firstname
, System.String lastname
, System.DateTime bDay
, System.Int32? departmentID
)
{
this.EmployeeID = employeeid;
this.FirstName = firstname;
this.LastName = lastname;
this.DateOfBirth = bDay;
this.DepartmentID = departmentID;
}
public Employee() { }
public System.String DetailsByLevel(int iLevel)
{
System.String s = "";
switch (iLevel)
{
case 1:
s = "Name:" + FirstName + " " + LastName + ".";
break;
case 2:
s = "Name: " + FirstName + " " + LastName +
", Birth Day: " + DateOfBirth.ToShortDateString() + ".";
break;
case 3:
s = "Name: " + FirstName + " " + LastName + ", Birth Day: " +
DateOfBirth.ToShortDateString() + ", Department:" +
DepartmentID.ToString() + ".";
break;
default:
break;
}
return s;
}
}
public interface IDepartment
{
System.Int32? DepartmentID { get; set; }
System.String Name { get; set; }
}
public class Department : IDepartment
{
#region Properties
public System.Int32? DepartmentID { get; set; }
public System.String Name { get; set; }
#endregion
public Department(
System.Int32? departmentid
, System.String name
)
{
this.DepartmentID = departmentid;
this.Name = name;
}
public Department() { }
}
For Employee
, there is a corresponding RepositoryEmployee
component to contain a collection of objects of Employee
.
For Department
, there is a corresponding RepositoryDepartment
component to contain a collection of objects of Department
. They are defined as follows.
public interface IRepository<T>
{
List<T> RepList { get; set; }
void GetAll();
DataSet DataSource { get; set; }
IUnityContainer IocContainer { get; set; }
AOPContainer AopContainer { get; set; }
}
public class RepositoryEmployee : IRepository<IEmployee>
{
public RepositoryEmployee()
{
}
private List<IEmployee> myList = null;
public List<IEmployee> RepList
{
get { return myList; }
set { myList = value; }
}
private IUnityContainer container = null;
public IUnityContainer IocContainer
{
get { return container; }
set { container = value; }
}
private AOPContainer aopcontainer = null;
public AOPContainer AopContainer
{
get { return aopcontainer; }
set { aopcontainer = value; }
}
private DataSet ds = null;
public DataSet DataSource
{
get { return ds; }
set { ds = value; }
}
public void GetAll()
{
System.Data.DataTable dt = ds.Tables["Employee"];
myList = container.Resolve<List<IEmployee>>();
foreach (System.Data.DataRow r in dt.Rows)
{
IEmployee iEmp = aopcontainer.Resolve<Employee, IEmployee>();
iEmp.EmployeeID = Convert.ToInt32(r["EmployeeID"]);
iEmp.FirstName = r["FirstName"].ToString();
iEmp.LastName = r["LastName"].ToString();
iEmp.DateOfBirth = Convert.ToDateTime(r["DateOfBirth"]);
iEmp.DepartmentID = Convert.ToInt32(r["DepartmentID"]);
myList.Add(iEmp);
}
}
}
public class RepositoryDepartment : IRepository<IDepartment>
{
public RepositoryDepartment()
{
}
private List<IDepartment> myList = null;
public List<IDepartment> RepList
{
get { return myList; }
set { myList = value; }
}
private IUnityContainer container = null;
public IUnityContainer IocContainer
{
get { return container; }
set { container = value; }
}
private AOPContainer aopcontainer = null;
public AOPContainer AopContainer
{
get { return aopcontainer; }
set { aopcontainer = value; }
}
private DataSet ds = null;
public DataSet DataSource
{
get { return ds; }
set { ds = value; }
}
public void GetAll()
{
System.Data.DataTable dt = ds.Tables["Department"];
myList = container.Resolve<List<IDepartment>>();
foreach (System.Data.DataRow r in dt.Rows)
{
IDepartment dep = aopcontainer.Resolve<Department, IDepartment>();
dep.DepartmentID = Convert.ToInt32(r["DepartmentID"]);
dep.Name = r["Name"].ToString();
myList.Add(dep);
}
}
}
Note that the data set is passed in using the DataSource
property of IRepository<T>
for both RepositoryEmployee
and RepositoryDepartment
. RepositoryEmployee
uses data from DataTable
Employee
of the data set while RepositoryDepartment
uses data from DataTable
Department
of the data set.
You can use either container
or aopcontainer
to create an object. The difference between them is that an object created
by aopcontainer
may have aspects attached while an object created by container
has no aspects.
Aspect Design
Aspects are cross-cutting concerns. For an AOP Container, an aspect is a method that takes an AspectContext
type object as its first parameter
and an object[]
type object as its second parameter and returns void
.
You may design your aspects in a more general way to use them in various situations. You may also design your aspects for some particular situations.
For example, you can define your entering/exiting log aspects in a general way and put them in the class SharedConcerns
as follows.
public class SharedConcerns
{
static SharedConcerns()
{
ConcernsContainer.runtimeAspects.Add("SharedLib.SharedConcerns.EnterLog",
new Decoration(SharedConcerns.EnterLog, null));
ConcernsContainer.runtimeAspects.Add("SharedLib.SharedConcerns.ExitLog",
new Decoration(SharedConcerns.ExitLog, null));
}
public static void EnterLog(AspectContext ctx, object[] parameters)
{
StackTrace st = new StackTrace(new StackFrame(4, true));
Console.Write(st.ToString());
IMethodCallMessage method = ctx.CallCtx;
string str = "Entering " + ctx.Target.GetType().ToString() +
"." + method.MethodName + "(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
public static void ExitLog(AspectContext ctx, object[] parameters)
{
IMethodCallMessage method = ctx.CallCtx;
string str = "Exiting " + ctx.Target.GetType().ToString() +
"." + method.MethodName + "(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
}
As you can see, these methods access Target
and CallCtx
in a general way, and can be shared by various types of objects to write entering/exiting logs.
On the other hand, some aspects may need to access more specific information. For example, you want to use WindowsPrincipal
for security checking.
The following code defines some specific aspects.
public class AppConcerns
{
class EmployeeAgeComparer : IComparer<IEmployee>
{
public int Compare(IEmployee e1, IEmployee e2)
{
return DateTime.Compare(e1.DateOfBirth, e2.DateOfBirth);
}
}
class EmployeeLastnameComparer : IComparer<IEmployee>
{
public int Compare(IEmployee e1, IEmployee e2)
{
return String.Compare(e1.LastName, e2.LastName);
}
}
static AppConcerns()
{
ConcernsContainer.runtimeAspects.Add(
"WpfApplication.AppConcerns.SecurityCheck",
new Decoration(AppConcerns.SecurityCheck, null));
ConcernsContainer.runtimeAspects.Add(
"WpfApplication.AppConcerns.SortEmployeeByLastname",
new Decoration(AppConcerns.SortEmployeeByLastname, null));
ConcernsContainer.runtimeAspects.Add(
"WpfApplication.AppConcerns.SortEmployeeByBirthday",
new Decoration(AppConcerns.SortEmployeeByBirthday, null));
}
public static void SecurityCheck(AspectContext ctx, object[] parameters)
{
Exception exInner = null;
try
{
if (parameters != null && parameters[0] is WindowsPrincipal &&
((WindowsPrincipal)parameters[0]).IsInRole("BUILTIN\\" +
"Administrators"))
{
return;
}
}
catch (Exception ex)
{
exInner = ex;
}
throw new Exception("No right to call!", exInner);
}
public static void SortEmployeeByLastname(AspectContext ctx, object[] parameters)
{
object target = ctx.Target;
if (target.GetType().ToString() == "WpfApplication.RepositoryEmployee")
{
List<IEmployee> emps = ((IRepository<IEmployee>)target).RepList;
IEnumerable<IEmployee> query = emps.OrderByDescending(emp => emp,
new EmployeeLastnameComparer()).ToList<IEmployee>();
((IRepository<IEmployee>)target).RepList = (List<IEmployee>)query;
}
}
public static void SortEmployeeByBirthday(AspectContext ctx, object[] parameters)
{
object target = ctx.Target;
if (target.GetType().ToString() == "WpfApplication.RepositoryEmployee")
{
List<IEmployee> emps = ((IRepository<IEmployee>)target).RepList;
IEnumerable<IEmployee> query = emps.OrderByDescending(emp => emp,
new EmployeeAgeComparer()).ToList<IEmployee>();
((IRepository<IEmployee>)target).RepList = (List<IEmployee>)query;
}
}
}
In the above code, the SecurityCheck
method can only check WindowsPrincipal
for security while SortEmployeeByLastname
and SortEmployeeByBirthday
can only be used by a target of RepositoryEmployee
.
Note: You may already have noticed that there is a static constructor in each of the classes SharedConcerns
and AppConcerns
.
Inside the static classes, each of the aspect methods defined in the classes is used to create an instance of Decoration
which is then added
to a dictionary ConcernsContainer.runtimeAspects
. The definition of ConcernsContainer
is as follows.
public class ConcernsContainer
{
static public Dictionary<string, Decoration> runtimeAspects =
new Dictionary<string, Decoration>();
}
The purpose of this Dictionary
and the static constructors is to keep an inventory for all Decoration
objects based on the aspect methods
defined in the application and make them accessible by the corresponding method names. It makes possible to configure aspects in the application configuration file
by specifying the corresponding method names.
Aspect Configuration
In the configuration file, you specify how aspects are associated with business components. Here are some examples to demonstrate how the aspects are configured.
<configuration>
<configSections>
<section name="DynamicDecoratorAspect"
type="DynamicDecoratorAOP.Configuration.DynamicDecoratorSection,
DynamicDecoratorAOP.Configuration" />
</configSections>
<DynamicDecoratorAspect>
<objectTemplates>
<add name="1"
type="WpfApplication.RepositoryDepartment"
interface="WpfApplication.IRepository`1[ThirdPartyHR.IDepartment]"
methods="GetAll"
predecoration="SharedLib.SharedConcerns.EnterLog,SharedLib"
postdecoration="SharedLib.SharedConcerns.ExitLog,SharedLib" />
<add name="2"
type="WpfApplication.RepositoryEmployee"
interface="WpfApplication.IRepository`1[ThirdPartyHR.IEmployee]"
methods="GetAll"
predecoration=""
postdecoration="WpfApplication.AppConcerns.SortEmployeeByLastname" />
<add name="3"
type="ThirdPartyHR.Department"
interface="ThirdPartyHR.IDepartment"
methods="DetailsByLevel,get_DepartmentID"
predecoration="SharedLib.SharedConcerns.EnterLog,SharedLib"
postdecoration="" />
</objectTemplates>
</DynamicDecoratorAspect>
</configuration>
First of all, you need to add a section <DynamicDecoratorAspect>
in your configuration file. You also need to add an applicationPath
key
to the <appSettings>
to specify the physical path of the Web application. It is required for a Web application since a Web application path is different
from the execution program path. Then, inside <objectTemplates>
of <DynamicDecoratorAspect>
, you add individual elements.
For each element inside <objectTemplates>
, the following attributes need to be specified:
type
- target typeinterface
- interface returnedmethods
- names of target methods which will be attached to the aspects specified by predecoration
and postdecoration
predecoration
- preprocessing aspectpostdecoration
- postprocessing aspect
Notes:
- The names in the value of the
methods
attribute are comma separated. For example, "DetailsByLevel,get_EmployeeID"
. - The value of the
predecoration
attribute has two parts and is separated by a comma. The first part specifies the aspect name while the second part
specifies the assembly name in which the aspect is defined, for example, "SharedLib.SharedConcerns.EnterLog,SharedLib"
. If the second
part is not specified, it is assumed that the aspect is defined in the entry assembly, for example, "WpfApplication.AppConcerns.SecurityCheck"
. - The value of the
postdecoration
attribute has two parts and is separated by a comma. The first part specifies the aspect name while the second part
specifies the assembly name in which the aspect is defined. If the second part is not specified, it is assumed that the aspect is defined in the entry assembly.
WPF Application Tasks
Before we apply the above business object model to a WPF application, we need to address several issues. WPF has limits on the binding
source (Binding Sources Overview). One issue is that WPF binding fails to work with
interface proxies (see WPF binding to a Windsor proxied
component and NHibernate, PropertyChanged event, and WPF).
So, it fails to work with the AOP Container which is based on interface proxies. Another issue is that to provide change notifications, a binding source
must implement the INotifyPropertyChanged
interface. Most types of business components do not implement this interface. Therefore, the business
object model developed above can not be used by a WPF application directly.
Intermediate Classes
To get around the above limits, intermediate classes are created to wrap our business objects. The intermediate classes should meet all the requirements
of a WPF data source while delegating all requests to the wrapped business objects. EmployeeProxy
and DepartmentProxy
are the intermediate
classes for business components Employee
and Department
, respectively, and are displayed as follows.
public class EmployeeProxy : IEmployee, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private IEmployee emp;
static public AOPContainer aopcontainer = null;
#region Properties
public System.Int32? EmployeeID
{
get { return emp.EmployeeID; }
set { emp.EmployeeID = value; OnPropertyChanged("EmployeeID"); }
}
public System.String FirstName
{
get { return emp.FirstName; }
set { emp.FirstName = value; OnPropertyChanged("FirstName"); }
}
public System.String LastName
{
get { return emp.LastName; }
set { emp.LastName = value; OnPropertyChanged("LastName"); }
}
public System.DateTime DateOfBirth
{
get { return emp.DateOfBirth; }
set { emp.DateOfBirth = value; OnPropertyChanged("DateOfBirth"); }
}
public System.Int32? DepartmentID
{
get { return emp.DepartmentID; }
set { emp.DepartmentID = value; OnPropertyChanged("DepartmentID"); }
}
#endregion
public System.String DetailsByLevel(int iLevel)
{
return emp.DetailsByLevel(iLevel);
}
public EmployeeProxy(IEmployee empOrig)
{
this.emp = empOrig;
}
public EmployeeProxy()
{
emp = aopcontainer.Resolve<Employee, IEmployee>();
}
}
EmployeeProxy
is a Proxy Pattern for Employee
. It provides the same interface as the original business object empOrig
and delegates all calls to the corresponding methods of the original object. It also implements INotifyPropertyChanged
so that whenever a property changes, the bound WPF control
will be notified. It also has a static public instance aopcontainer
of type AOPContainer
. The aopcontainer
is used to create a new business
object in the default constructor. The default constructor is called by a bound WPF user control when adding a new entry from the user interface.
Instead of explicitly putting an OnPropertyChanged
call in each of the property set methods, PostSharp can be used to inject the code in each
of the property set methods. I am not going to discuss it in detail here. However, the download AopContainerWpfWithPostSharp.zip uses PostSharp to inject
the call to the property set methods. You will need to install PostSharp to get it to work.
public class DepartmentProxy : IDepartment
{
#region Properties
public System.Int32? DepartmentID
{
get { return dep.DepartmentID; }
set { dep.DepartmentID = value; }
}
public System.String Name
{
get { return dep.Name; }
set { dep.Name = value; }
}
#endregion
private IDepartment dep;
public DepartmentProxy(IDepartment depOrig)
{
this.dep = depOrig;
}
}
DepartmentProxy
is a Proxy Pattern for Department
, which provides the same interface as the original business object depOrig
.
However, it doesn't implement the INotifyPropertyChanged
interface, which means it will not send change notifications to the bound WPF user control.
It doesn't have a default constructor, which means the bound WPF user control will not be able to add a new entry from the user interface.
Now, we are ready to work on the WPF application.
Application Class
A few application wide setups/cleanups need to be addressed at the application level. Here is the code of the App
class:
public partial class App : Application
{
private FileStream fileStream = null;
private TextWriter tmp = null;
private StreamWriter sw1 = null;
static public IUnityContainer iocContainer = null;
static public AOPUnityContainer aopContainer = null;
void App_Startup(object sender, StartupEventArgs e)
{
string path = ".";
if (!File.Exists(path + "\\hrlog.txt"))
{
fileStream = new FileStream(path + "\\hrlog.txt", FileMode.Create);
}
else
fileStream = new FileStream(path + "\\hrlog.txt", FileMode.Truncate);
tmp = Console.Out;
sw1 = new StreamWriter(fileStream);
Console.SetOut(sw1);
App.iocContainer = new UnityContainer();
App.iocContainer.RegisterType<IEmployee, Employee>(new InjectionConstructor());
App.iocContainer.RegisterType<IDepartment,
Department>(new InjectionConstructor());
App.iocContainer.RegisterType<IRepository<IDepartment>,
RepositoryDepartment>(new InjectionConstructor());
App.iocContainer.RegisterType<IRepository<IEmployee>,
RepositoryEmployee>(new InjectionConstructor());
App.iocContainer.RegisterType<IList<IEmployee>,
List<IEmployee>>(new InjectionConstructor());
App.iocContainer.RegisterType<IList<IDepartment>,
List<IDepartment>>(new InjectionConstructor());
App.aopContainer = new AOPUnityContainer(App.iocContainer);
Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
Decoration dec =
ConcernsContainer.runtimeAspects["WpfApplication.AppConcerns.SecurityCheck"];
dec.Parameters = new object[] { Thread.CurrentPrincipal };
}
void App_Exit(object sender, ExitEventArgs e)
{
Console.SetOut(tmp);
sw1.Close();
fileStream.Close();
IDisposable container = App.iocContainer;
container.Dispose();
}
}
In App_Startup
, first, we redirect the console output to a file. This way, the aspects EnterLog
and ExitLog
will write logs to the file instead of the console. Then, we create a UnityContainer
object and register a few types to it. An AOPUnityContainer
object
is also created. Both the UnityContainer
object and the AOPUnityContainer
are assigned to the corresponding public static members, respectively,
and will be used in other places of the application. We also set the Parameters
property of the SecurityCheck
aspect to WindowsPrincipal
so that we can check security during the execution of the application.
The code in App_Exit
does some cleanups.
Main Window
The Main Window is the GUI for the application. Its XAML code is listed as follows.
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="402*" />
<ColumnDefinition Width="101*" />
</Grid.ColumnDefinitions>
<ComboBox Height="23"
HorizontalAlignment="Left" Margin="34,33,0,0"
Name="comboBox1" VerticalAlignment="Top" Width="120"
SelectionChanged="comboBox1_SelectionChanged"/>
<DataGrid AutoGenerateColumns="False" Height="200"
HorizontalAlignment="Left" Margin="34,82,0,0"
Name="dataGrid1" VerticalAlignment="Top"
Width="435" Grid.ColumnSpan="2"
CanUserAddRows="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Employee ID"
Binding="{Binding EmployeeID}" />
<DataGridTextColumn Header="First Name"
Binding="{Binding FirstName}" />
<DataGridTextColumn Header="Last Name"
Binding="{Binding LastName, Mode=TwoWay}" />
<DataGridTextColumn Header="Birth Date"
Binding="{Binding DateOfBirth}" />
<DataGridTextColumn Header="DepartmentID"
Binding="{Binding DepartmentID}" />
</DataGrid.Columns>
</DataGrid>
<Label Content="Departments" Height="28"
HorizontalAlignment="Left" Margin="34,12,0,0"
Name="label1" VerticalAlignment="Top"
Width="84" IsEnabled="True" />
<Button Content="PropertyChanged" Height="23"
HorizontalAlignment="Left" Margin="188,33,0,0"
Name="button1" VerticalAlignment="Top"
Width="106" Click="button1_Click" />
<Button Content="CollectionChanged" Grid.ColumnSpan="2"
Height="23" HorizontalAlignment="Left"
Margin="330,35,0,0" Name="button2"
VerticalAlignment="Top" Width="139"
Click="button2_Click" />
<Button Content="Save" Height="23"
HorizontalAlignment="Left" Margin="204,288,0,0"
Name="button3" VerticalAlignment="Top"
Width="75" Click="button3_Click" />
<Label Content="Employees" Height="28"
HorizontalAlignment="Left" Margin="34,60,0,0"
Name="label2" VerticalAlignment="Top" Width="84" />
</Grid>
</Window>
The MainWindow
has a ComboBox to list the departments and a DataGrid
to list employees. It has also three Button
s for testing property change,
collection change, and saving changes, respectively.
The C# code for the MainWindow
is listed as follows.
public partial class MainWindow : Window
{
private ObservableCollection<EmployeeProxy> lstEmp = null;
private static int iStaticDep = 0;
DataSet dsDB = new DataSet();
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
try
{
dsDB.ReadXml("employees.xml");
IRepository<IDepartment> rpDepartment =
App.aopContainer.Resolve<RepositoryDepartment, IRepository<IDepartment>>();
rpDepartment.IocContainer = App.iocContainer;
rpDepartment.AopContainer = App.aopContainer;
rpDepartment.DataSource = dsDB;
rpDepartment.GetAll();
List<DepartmentProxy> lstDep = new List<DepartmentProxy>();
foreach (IDepartment d in rpDepartment.RepList)
{
lstDep.Add(new DepartmentProxy(d));
}
comboBox1.ItemsSource = lstDep;
comboBox1.DisplayMemberPath = "Name";
comboBox1.SelectedValuePath = "DepartmentID";
IRepository<IEmployee> rpEmployee =
App.aopContainer.Resolve<RepositoryEmployee, IRepository<IEmployee>>();
rpEmployee.IocContainer = App.iocContainer;
rpEmployee.AopContainer = App.aopContainer;
rpEmployee.DataSource = dsDB;
rpEmployee.GetAll();
EmployeeProxy.aopcontainer = App.aopContainer;
lstEmp = new ObservableCollection<EmployeeProxy>();
foreach (IEmployee em in rpEmployee.RepList)
{
lstEmp.Add(new EmployeeProxy(em));
}
dataGrid1.ItemsSource = lstEmp;
}
catch (Exception ex)
{
throw ex;
}
}
private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (comboBox1.SelectedValue != null)
{
IDepartment dpSel = (IDepartment)comboBox1.SelectedItem;
iStaticDep = dpSel.DepartmentID.Value;
List<EmployeeProxy> empSel = null;
if (lstEmp != null)
{
empSel = lstEmp.ToList<EmployeeProxy>().FindAll(
(EmployeeProxy emp) => { return emp.DepartmentID.Value == iStaticDep; });
}
dataGrid1.ItemsSource = new ObservableCollection<EmployeeProxy>(empSel);
}
}
private void button1_Click(object sender, RoutedEventArgs e)
{
try
{
IEmployee oEm = lstEmp[0];
oEm = ObjectProxyFactory.CreateProxy<IEmployee>(
oEm,
new String[] { "set_EmployeeID" },
ConcernsContainer.runtimeAspects["WpfApplication.AppConcerns.SecurityCheck"],
null);
if (lstEmp[0].EmployeeID == 100)
oEm.EmployeeID = 1;
else
oEm.EmployeeID = 100;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void button2_Click(object sender, RoutedEventArgs e)
{
EmployeeProxy empCollectionChanged = null;
empCollectionChanged = lstEmp.ToList<EmployeeProxy>().Find(
(EmployeeProxy em) => { return em.EmployeeID.Value == 1001; });
if (empCollectionChanged != null)
lstEmp.Remove(empCollectionChanged);
else
{
empCollectionChanged = new EmployeeProxy();
empCollectionChanged.EmployeeID = 1001;
empCollectionChanged.FirstName = "John";
empCollectionChanged.LastName = "Doe";
empCollectionChanged.DateOfBirth = new DateTime();
empCollectionChanged.DepartmentID = 1;
lstEmp.Add(empCollectionChanged);
}
}
private void button3_Click(object sender, RoutedEventArgs e)
{
System.Data.DataSet ds = dsDB;
System.Data.DataTable dt = ds.Tables["Employee"];
dt.Clear();
foreach (EmployeeProxy emp in lstEmp)
{
System.Data.DataRow r = dt.NewRow();
r["EmployeeID"] = emp.EmployeeID == null ? -1 : emp.EmployeeID;
r["FirstName"] = emp.FirstName == null ? "" : emp.FirstName;
r["LastName"] = emp.LastName == null ? "" : emp.LastName;
r["DateOfBirth"] = emp.DateOfBirth;
r["DepartmentID"] = emp.DepartmentID == null ? -1 : emp.DepartmentID;
dt.Rows.Add(r);
}
dsDB.WriteXml("employees.xml");
}
}
In the event handler Window_Loaded
, an XML file employees.xml is loaded into the DataSet
dsDB
.
This XML file persists data for departments and employees and plays a database role for the application. Of course, you can use a real database and its tables
to persist data and load them into the DataSet
.
Note that rpDepartment.RepList
is a list of interface proxies, we need to convert it to a list of DepartmentProxy
objects
and bind the list of DepartmentProxy
to comboBox1
. For the same reason, we have to convert rpEmployee.RepList
to a list of EmployeeProxy
objects and bind the list of EmployeeProxy
to dataGrid1
. Note also that
a ObservableCollection<EmployeeProxy>
is assigned to dataGrid1
, which means notifications will be generated when items
get added to or removed from the collection.
The event handler comboBox1_SelectionChanged
is executed when a department is selected. Then, the employees associated with the department are displayed.
The event handler button1_Click
is executed when pressing the PropertyChanged
button. The code toggles
the first employee's EmployeeID between 1 and 100. Note that ObjectProxyFactory.CreateProxy
is called to add the security check before changing the EmployeeID.
The important point here is that you can chain aspects to object in the code no matter whether it is configured in the configuration file or not.
The best practice for this scenario is that since your application may have a lot of employees and an employee may have a lot of properties, it is better
to add aspects to an employee object using code as needed instead of configuring aspects to the employee component in the configuration file.
The event handler button2_Click
is executed when pressing the CollectionChanged
button. The code simply adds or removes
an employee from the employee collection with EmployeeID 1001.
The event handler button3_Click
is executed when pressing the Save button. The code persists changes of employees to the "employees.xml" file.
Run the application, the MainWindow
looks as follows:
When a department is selected, the employees for that department are displayed.
Close the application and open the file hrlog.txt. You will see the following logs:
at WpfApplication.MainWindow.Window_Loaded(Object sender, RoutedEventArgs e)
in C:\AopContainerWpf\WpfApplication\MainWindow.xaml.cs:line 146
Entering WpfApplication.RepositoryDepartment.GetAll()
Exiting WpfApplication.RepositoryDepartment.GetAll()
at WpfApplication.DepartmentProxy.get_DepartmentID() in
C:\AopContainerWpf\WpfApplication\Model\DepartmentProxy.cs:line 14
Entering ThirdPartyHR.Department.get_DepartmentID()
at WpfApplication.DepartmentProxy.get_DepartmentID() in
C:\AopContainerWpf\WpfApplication\Model\DepartmentProxy.cs:line 14
Entering ThirdPartyHR.Department.get_DepartmentID()
Run it again. Press the PropertyChanged button, the EmployeeID of the first record is set to 100. The MainWindow
is updated to show the change as follows.
Press the CollectionChanged button, a new employee record is added with EmployeeID 1001. The MainWindow
is updated to show the change as follows.
Close the application and open the file hrlog.txt. You will see the following logs.
at WpfApplication.MainWindow.Window_Loaded(Object sender, RoutedEventArgs e)
in C:\AopContainerWpf\WpfApplication\MainWindow.xaml.cs:line 146
Entering WpfApplication.RepositoryDepartment.GetAll()
Exiting WpfApplication.RepositoryDepartment.GetAll()
Now, comment out the line Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
in App_Startup
of the App
class.
Build and run the application. When the MainWindow
is up, click the PropertyChanged button, and you will see the following message:
You were not allowed to change the EmployeeID since you failed the security check.
As you may have noticed, the above application sorts the employee by the employee's last name. What if you want to sort employees by their birthday?
You change the configuration entry for WpfApplication.RepositoryEmployee
by replacing WpfApplication.AppConcerns.SortEmployeeByLastname
with WpfApplication.AppConcerns.SortEmployeeByBirthday
. That's it. Now, your application sorts employees by their birthday.
Points of Interest
AOP Container makes extending functionality of business components or adding AOP capabilities to them very easy. You define your aspect methods, and configure them
in the configuration file to associate aspects to business components.
AOP Container makes component design, aspect design, and application design completely decoupled. Configuration settings and global settings wire all the pieces together.
Changing/Extending functionality may be as simple as changing configuration settings.
You can also mix adding aspects using configuration and adding aspects using code. This way, you can use aspect configuration for general purpose while using
code to chain more aspects to meet your specific requirements as needed.
The Proxy Pattern is a neat way to apply the power of WPF to business objects based on interface interception technology.
Although this article discusses AOP container with Unity IoC container, you should be able to easily combine an AOP Container with other IoC containers like MEF and Windsor.