Introduction
The reason for this article is that I have read many other articles on Dependency Injection (DI) and Inversion Of Control (IOC), and felt there was a lack of practical examples of its implementation. So I decided to create a project which used Dependency Injection (DI) to switch between two data sources, one from a database, and one from a mocked data source. I thought my idea could hopefully show developers looking for a practical way to use DI a simple example which may go some way to seem like a real world scenario.
This project was created using Visual Studio 2013, it has been tested with Visual Studio 2012 and will run and compile, but there is an error message 168 which comes from Entity Framework 6. Apparently this is a known bug.
Also, you will get a message relating to Team Foundation Server, please ignore it. I have removed the .vssscc files, but it persists.
Note: For instructions on how to create the database from the database project included please see the section - Instructions for Creating The Database near the end of the article.
Project Description
The project consists of four class libraries, a WPF host application, and a database project for creating the database used for the 'Live Data' mode of operation.
The project structure can be seen below:
Firstly, I will describe the Dependency.Injection.Database
project. This project when published creates the database, and populates it with data on your local SQL Server database instance. The database used is the Northwind database provided by Microsoft in the 1990s as a test database.
Note: I have only tested the database project with a SQL Server 2012 DBMS, although I am fairly sure it will be fine when used with earlier versions.
Next, I will describe the Dependency.Injection.DataModel
class library. This class library consists of an Entity Framework 6 Entity Data Model, and associated classes. The Entity Data Model was created from a Northwind database installed on my local SQL Server 2012 DBMS.
Next, I will describe the Dependency.Injection.DataAccess
class library. This is where the database operations are conducted by accessing the NorthWindEntities
class created in the Dependency.Injection.DataModel
class library.
Next, I will describe the Dependency.Injection.DataMocking
class library. This is used to create mocked data, the method for which will be described in the 'Using the Code' section.
Next, I will describe the Dependency.Injection.DataClient.
This is where the Dependency.Injection.DataAccess
classes are consumed. Also, there is a class to access the Dependency.Injection.DataMocking
class library described above.
Lastly, I will describe the Dependency.Injection.WPFHost
. This is the WPF host application for the project.
Using the Code
Starting with the DataModel
class library, all the code in this project was created automatically by Visual Studio when the Entity Data Model was created. This includes all the classes used to mimic the database structure. The edmx data structure can be seen below:
The diagram above represents the database exactly. One of the classes created is shown below:
public partial class Order
{
public Order()
{
this.Order_Details = new HashSet<Order_Detail>();
}
public int OrderID { get; set; }
public string CustomerID { get; set; }
public Nullable<int> EmployeeID { get; set; }
public Nullable<System.DateTime> OrderDate { get; set; }
public Nullable<System.DateTime> RequiredDate { get; set; }
public Nullable<System.DateTime> ShippedDate { get; set; }
public Nullable<int> ShipVia { get; set; }
public Nullable<decimal> Freight { get; set; }
public string ShipName { get; set; }
public string ShipAddress { get; set; }
public string ShipCity { get; set; }
public string ShipRegion { get; set; }
public string ShipPostalCode { get; set; }
public string ShipCountry { get; set; }
public virtual Customer Customer { get; set; }
public virtual Employee Employee { get; set; }
public virtual ICollection<Order_Detail> Order_Details { get; set; }
public virtual Shipper Shipper { get; set; }
}
These classes provide suitable types and collections to access the Northwind database.
The DataAccess
project only contains one class, which is shown below:
public class NorthwindDataAccess
{
NorthwindEntities context = null;
public NorthwindDataAccess()
{
context = new NorthwindEntities();
}
public List<Category> GetCategories()
{
try
{
List<Category> categories = context.Categories.ToList();
return categories;
}
catch (Exception ex)
{
return null;
}
}
public List<Customer> GetCustomers()
{
try
{
List<Customer> customers = context.Customers.ToList();
return customers;
}
catch (Exception ex)
{
return null;
}
}
public List<CustomerDemographic> GetCustomerDemographics()
{
try
{
List<CustomerDemographic> customerDemographics = context.CustomerDemographics.ToList();
return customerDemographics;
}
catch (Exception ex)
{
return null;
}
}
public List<Employee> GetEmployees()
{
try
{
List<Employee> employees = context.Employees.ToList();
return employees;
}
catch (Exception ex)
{
return null;
}
}
public List<Order> GetOrders()
{
try
{
List<Order> orders = context.Orders.ToList();
return orders;
}
catch (Exception ex)
{
return null;
}
}
public List<Order_Detail> GetOrderDetails()
{
try
{
List<Order_Detail> orderDetails = context.Order_Details.ToList();
return orderDetails;
}
catch (Exception ex)
{
return null;
}
}
public List<Product> GetProducts()
{
try
{
List<Product> products = context.Products.ToList();
return products;
}
catch (Exception ex)
{
return null;
}
}
public List<Region> GetRegions()
{
try
{
List<Region> regions = context.Regions.ToList();
return regions;
}
catch (Exception ex)
{
return null;
}
}
public List<Shipper> GetShippers()
{
try
{
List<Shipper> shippers = context.Shippers.ToList();
return shippers;
}
catch (Exception ex)
{
return null;
}
}
public List<Supplier> GetSuppliers()
{
try
{
List<Supplier> suppliers = context.Suppliers.ToList();
return suppliers;
}
catch (Exception ex)
{
return null;
}
}
public List<Territory> GetTerritories()
{
try
{
List<Territory> territories = context.Territories.ToList();
return territories;
}
catch (Exception ex)
{
return null;
}
}
}
The NorthwindDataAccess
class uses an instance of the NorthwindEntities
class to access the database. Each table in the database is converted to a generic list, and returned.
Note: The reason for returning the whole data list may seem strange, but as lazy loading is used, the data is not fetched from the database until it is needed.
The DataClient
class library provides the classes which deliver the two types of data; the data from the database or 'Live Data', and the mocked data.
The class which delivers the 'Live Data' is shown below:
public class DatabaseDirectClient : IData
{
NorthwindDataAccess dBAccess = null;
Northwind data = null;
public DatabaseDirectClient(Boolean getAllData)
{
dBAccess = new NorthwindDataAccess();
if (getAllData) Data = GetNorthwindData();
}
public Northwind Data
{
get
{
if (data == null)
data = GetNorthwindData();
return data;
}
set
{
data = value;
}
}
private Northwind GetNorthwindData()
{
try
{
Northwind northwindData = new Northwind();
northwindData.CustomerData = new Lazy<List<Customer>>(() => dBAccess.GetCustomers());
northwindData.CategoryData = new Lazy<List<Category>>(() => dBAccess.GetCategories());
northwindData.CustomerDemographicData = new Lazy<List<CustomerDemographic>>(() => dBAccess.GetCustomerDemographics());
northwindData.EmployeeData = new Lazy<List<Employee>>(() => dBAccess.GetEmployees());
northwindData.OrderData = new Lazy<List<Order>>(() => dBAccess.GetOrders());
northwindData.OrderDetailData = new Lazy<List<Order_Detail>>(() => dBAccess.GetOrderDetails());
northwindData.ProductData = new Lazy<List<Product>>(() => dBAccess.GetProducts());
northwindData.RegionData = new Lazy<List<Region>>(() => dBAccess.GetRegions());
northwindData.ShipperData = new Lazy<List<Shipper>>(() => dBAccess.GetShippers());
northwindData.SupplierData = new Lazy<List<Supplier>>(() => dBAccess.GetSuppliers());
northwindData.TerritoryData = new Lazy<List<Territory>>(() => dBAccess.GetTerritories());
return northwindData;
}
catch (Exception ex)
{
return null;
}
}
}
This class inherits from the interface IData
which is used to resolve the types on the IOC container in the host application.
As can be seen, an instance of the NorthwindDataAccess
class is used to acquire all the data from the Northwind database, using lazy loaded methods.
The class which delivers the mocked data is shown below:
public class MockedDataClient : IData
{
Northwind data = null;
private Int32 numberOfRecords = 0;
public MockedDataClient(Int32 numberOfRecords)
{
this.numberOfRecords = numberOfRecords;
}
public Northwind Data
{
get
{
if (data == null)
data = GetNorthwindData();
return data;
}
set
{
data = value;
}
}
private Northwind GetNorthwindData()
{
Northwind northwindData = new Northwind();
northwindData.CategoryData = new Lazy<List<Category>> (() => MockData.GetCategories(numberOfRecords));
northwindData.CustomerData = new Lazy<List<Customer>> (() => MockData.GetCustomers(numberOfRecords));
northwindData.CustomerDemographicData = new Lazy<List<CustomerDemographic>>(() => MockData.GetCustomerDemographics(numberOfRecords));
northwindData.EmployeeData = new Lazy<List<Employee>>(() => MockData.GetEmployees(numberOfRecords));
northwindData.OrderData = new Lazy<List<Order>> (() => MockData.GetOrders(numberOfRecords));
northwindData.OrderDetailData = new Lazy<List<Order_Detail>> (() => MockData.GetOrderDetails(numberOfRecords));
northwindData.ProductData = new Lazy<List<Product>> (() => MockData.GetProducts(numberOfRecords));
northwindData.RegionData = new Lazy<List<Region>> (() => MockData.GetRegions(numberOfRecords));
northwindData.ShipperData = new Lazy<List<Shipper>> (() => MockData.GetShippers(numberOfRecords));
northwindData.SupplierData = new Lazy<List<Supplier>> (() => MockData.GetSuppliers(numberOfRecords));
northwindData.TerritoryData = new Lazy<List<Territory>> (() => MockData.GetTerritories(numberOfRecords));
return northwindData;
}
}
As can be seen, the class structure is the same, but the data comes from static
methods inside the MockData
class which will be described later.
There are two other class in the DataClient
class library, the Northwind
class is shown below:
public class Northwind
{
public Lazy<List<Category>> CategoryData { get; set; }
public Lazy<List<Customer>> CustomerData { get; set; }
public Lazy<List<CustomerDemographic>> CustomerDemographicData { get; set; }
public Lazy<List<Employee>> EmployeeData { get; set; }
public Lazy<List<Order>> OrderData { get; set; }
public Lazy<List<Order_Detail>> OrderDetailData { get; set; }
public Lazy<List<Product>> ProductData { get; set; }
public Lazy<List<Region>> RegionData { get; set; }
public Lazy<List<Shipper>> ShipperData { get; set; }
public Lazy<List<Supplier>> SupplierData { get; set; }
public Lazy<List<Territory>> TerritoryData { get; set; }
}
This is used as a class to hold the data in lazy properties. The last class in this class library is the OrdersData
class shown below:
public class OrdersData
{
public DateTime? OrderDate { get; set; }
public String ProductName { get; set; }
public Int32? Quantity { get; set; }
public Decimal Price { get; set; }
public Decimal TotalCost { get; set; }
}
This class is used in the host application to display data in a DataGrid
which is derived from the database.
The DataMocking
class library has only got one class in it, shown below:
public class MockData
{
public static List<Category> GetCategories(int size)
{
List<Category> categories = Builder<Category>.CreateListOfSize(size).Build().ToList();
return categories;
}
public static List<Customer> GetCustomers(int size)
{
List<Customer> customers = Builder<Customer>.CreateListOfSize(size).Build().ToList();
return customers;
}
public static List<CustomerDemographic> GetCustomerDemographics(int size)
{
List<CustomerDemographic> customerDemographics = Builder<CustomerDemographic>.CreateListOfSize(size).Build().ToList();
return customerDemographics;
}
public static List<Employee> GetEmployees(int size)
{
List<Employee> employess = Builder<Employee>.CreateListOfSize(size).Build().ToList();
return employess;
}
public static List<Order> GetOrders(int size)
{
List<Order> orders = Builder<Order>.CreateListOfSize(size).Build().ToList();
return orders;
}
public static List<Order_Detail> GetOrderDetails(int size)
{
List<Order_Detail> orderDetails = Builder<Order_Detail>.CreateListOfSize(size).Build().ToList();
return orderDetails;
}
public static List<Product> GetProducts(int size)
{
List<Product> products = Builder<Product>.CreateListOfSize(size).Build().ToList();
return products;
}
public static List<Region> GetRegions(int size)
{
List<Region> regions = Builder<Region>.CreateListOfSize(size).Build().ToList();
return regions;
}
public static List<Shipper> GetShippers(int size)
{
List<Shipper> shippers = Builder<Shipper>.CreateListOfSize(size).Build().ToList();
return shippers;
}
public static List<Supplier> GetSuppliers(int size)
{
List<Supplier> suppliers = Builder<Supplier>.CreateListOfSize(size).Build().ToList();
return suppliers;
}
public static List<Territory> GetTerritories(int size)
{
List<Territory> territories = Builder<Territory>.CreateListOfSize(size).Build().ToList();
return territories;
}
}
As can be seen, this class uses the NBuilder
extension found in the NuGet package manager to generate mocked data.
WPF Host Application
The structure for the host application is shown below:
The programming pattern used is the MVVM pattern, this is achieved with the use of a View Model, and a Command
class.
The View Model consists of two class in an inheritance configuration. The ViewModelBase
class is shown below:
class ViewModelBase : INotifyPropertyChanged
{
protected Northwind NorthwindData = null;
protected IUnityContainer container;
public ViewModelBase()
{
container = new UnityContainer();
ContainerBootStrapper.RegisterTypes(container);
NorthwindData = container.Resolve<IData>("MockedData").Data;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Private Methods
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
This class contains the INotifyPropertyChanged
interface members required to update the view (MainWindow.xaml). As can be seen, the NorthwindData
member is populated with the 'MockedData
' by resolving the mocked data named registration on the IOC container.
Also the ContainerBootStrapper.RegisterTypes
is called here in the constructor, which is shown below:
class ContainerBootStrapper
{
public static void RegisterTypes(IUnityContainer container)
{
container.RegisterType<IData, MockedDataClient>("MockedData",
new TransientLifetimeManager(),
new InjectionConstructor(10));
container.RegisterType<IData, DatabaseDirectClient>("DbData",
new TransientLifetimeManager(),
new InjectionConstructor(false));
}
}
The container type used is a Microsoft.Practices.Unity
container. There are two named configurations used, 'DbData', and 'MockedData'. Each registers a different class which inherits from IData
interface as explained earlier. Both registrations use a TransientLifetimeManager
which gives a different instance of the class each time it is resolved. Finally, the appropriate InjectionConstructor
parameters are injected.
In the ViewModelBase
, the class NorthwindData
is instantiated here by resolving the IData
interface with the named configuration 'MockedData
'. This ensures the most reliable data source is selected at start-up.
The MainViewModel
class inherits from the ViewModelBase
class, and is shown below:
class MainViewModel : ViewModelBase
{
#region Construction
public MainViewModel()
{
TabSelectionChangedCommand = new Command(GetTabSelectionChangedCommandExecute, GetTabSelectionChangedCommandCanExecute);
ComboDatasourceChangedCommand = new Command(GetComboDatasourceChangedCommandExecute, GetComboDatasourceChangedCommandCanExecute);
ComboCustomerChangedCommand = new Command(GetComboCustomerChangedCommandExecute, GetComboCustomerChangedCommandCanExecute);
CustomerData = NorthwindData.CustomerData.Value.ToObservableCollection();
EmployeeData = NorthwindData.EmployeeData.Value.ToObservableCollection();
if (!DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
Customers = DataOperations.GetCustomers(NorthwindData);
CustomerOrdersData = DataOperations.GetCustomerOrders(NorthwindData, NorthwindData.CustomerData.Value.Select(x => x.CustomerID).FirstOrDefault()).ToObservableCollection();
}
}
#endregion
#region Commands
public Command TabSelectionChangedCommand { get; set; }
public Command ComboDatasourceChangedCommand { get; set; }
public Command ComboCustomerChangedCommand { get; set; }
private Boolean GetComboCustomerChangedCommandCanExecute(Object parameter)
{
return true;
}
private void GetComboCustomerChangedCommandExecute(Object parameter)
{
if (parameter == null) return;
Tuple<String, String> customerData = (Tuple<String, String>)parameter;
String customerID = customerData.Item1;
InvokeMethodThreaded(new Action(() =>
{
CustomerOrdersData = DataOperations.GetCustomerOrders(NorthwindData, customerID);
}));
}
private Boolean GetComboDatasourceChangedCommandCanExecute(Object parameter)
{
return true;
}
private void GetComboDatasourceChangedCommandExecute(Object parameter)
{
ComboBoxItem comboBox = (ComboBoxItem)parameter;
String selection = comboBox.Content.ToString();
if (selection == "Mocked Data")
NorthwindData = container.Resolve<IData>("MockedData").Data;
if (selection == "Live Data")
{
InvokeMethodThreaded(new Action(() =>
{
NorthwindData = container.Resolve<IData>("DbData").Data;
}));
}
TabSelectedIndex = -1;
}
private Boolean GetTabSelectionChangedCommandCanExecute(Object parameter)
{
return true;
}
private void GetTabSelectionChangedCommandExecute(Object parameter)
{
if (parameter == null) return;
TabItem tabItem = (TabItem)parameter;
if (tabItem.Header.ToString() == "Customers")
{
InvokeMethodThreaded(new Action(() =>
{
CustomerData = NorthwindData.CustomerData.Value.ToObservableCollection();
}));
}
if (tabItem.Header.ToString() == "Customer Orders")
{
InvokeMethodThreaded(new Action(() =>
{
Customers = DataOperations.GetCustomers(NorthwindData).ToObservableCollection();
CustomerOrdersData = new ObservableCollection<OrdersData>();
}));
}
if (tabItem.Header.ToString() == "Employees")
{
InvokeMethodThreaded(new Action(() =>
{
EmployeeData = NorthwindData.EmployeeData.Value.ToObservableCollection();
}));
}
}
#endregion
#region Properties
private Int32 _tabSelectedIndex = 0;
public Int32 TabSelectedIndex
{
set
{
_tabSelectedIndex = value;
RaisePropertyChanged("TabSelectedIndex");
}
}
private ObservableCollection<OrdersData> _customerOrdersData = new ObservableCollection<OrdersData>();
public ObservableCollection<OrdersData> CustomerOrdersData
{
get
{
return _customerOrdersData;
}
set
{
_customerOrdersData = value;
RaisePropertyChanged("CustomerOrdersData");
}
}
private ObservableCollection<Tuple<string, string>> _customers = new ObservableCollection<Tuple<string, string>>();
public ObservableCollection<Tuple<string, string>> Customers
{
get
{
return _customers;
}
set
{
_customers = value;
RaisePropertyChanged("Customers");
}
}
private ObservableCollection<Customer> _customerData = new ObservableCollection<Customer>();
public ObservableCollection<Customer> CustomerData
{
get
{
return _customerData;
}
set
{
_customerData = value;
RaisePropertyChanged("CustomerData");
}
}
private ObservableCollection<Employee> _employeeData = new ObservableCollection<Employee>();
public ObservableCollection<Employee> EmployeeData
{
get
{
return _employeeData;
}
set
{
_employeeData = value;
RaisePropertyChanged("EmployeeData");
}
}
#endregion
#region Visibility
private Visibility _busyVisibility = Visibility.Hidden;
public Visibility BusyVisibility
{
get
{
return _busyVisibility;
}
set
{
_busyVisibility = value;
RaisePropertyChanged("BusyVisibility");
}
}
#endregion
#region Private Methods
private void UISetBusy()
{
BusyVisibility = Visibility.Visible;
}
private void UIClearBusy()
{
BusyVisibility = Visibility.Hidden;
}
private void InvokeMethodThreaded(Action actionToExecute)
{
Thread t = new Thread(delegate()
{
UISetBusy();
actionToExecute();
UIClearBusy();
});
t.Start();
}
#endregion
}
There are three instances of the Command
class. They are used to handle events from the view, and make the appropriate actions. An explanation of the Command
class can be found in my article called WPF Secure Messenger, where it is also used.
When the application has been started, the following window will appear:
At start-up, the application loads into the first DataGrid
control mocked Employees
data, as described earlier. The MainWindow.xaml file is shown below:
<Window x:Class="Dependency.Injection.WPFHost.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Dependency.Injection.WPFHost"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Title="Dependency Injection Data Demo" Height="600" Width="1000" ResizeMode="NoResize"
Icon="Injection.ico" >
<Window.Background>
<LinearGradientBrush>
<GradientStop Color="LightSteelBlue" Offset="0.5" />
<GradientStop Color="LightBlue" Offset="1" />
</LinearGradientBrush>
</Window.Background>
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="780*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="90" />
<RowDefinition Height="460*" />
</Grid.RowDefinitions>
<ComboBox x:Name="comboDataSource" Grid.Column="0" Grid.Row="0" Width="100" Height="22" HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="30,32" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged" >
<i:InvokeCommandAction Command="{Binding ComboDatasourceChangedCommand}"
CommandParameter="{Binding ElementName=comboDataSource, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ComboBoxItem Content="Mocked Data" Selector.IsSelected="True" />
<ComboBoxItem Content="Live Data" />
</ComboBox>
<GroupBox Grid.Column="0" Grid.Row="0" Width="150" Height="60" HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="10" Header="Data Source"/>
<TabControl x:Name="tabControl" Grid.Column="0" Grid.Row="1" Margin="10"
SelectedIndex="{Binding TabSelectedIndex, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding TabSelectionChangedCommand}"
CommandParameter="{Binding ElementName=tabControl, Path=SelectedItem}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TabItem Header="Employees">
<DataGrid ItemsSource="{Binding Path=EmployeeData}" AutoGenerateColumns="False" AlternatingRowBackground="LightSteelBlue">
<DataGrid.Columns>
<DataGridTextColumn Width="*" Binding="{Binding Path=FirstName}" Header="First Name" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=LastName}" Header="Last Name" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=Title}" Header="Title" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=Extension}" Header="Extension" IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Customers">
<DataGrid ItemsSource="{Binding Path=CustomerData}" AutoGenerateColumns="False" AlternatingRowBackground="LightSteelBlue">
<DataGrid.Columns>
<DataGridTextColumn Width="*" Binding="{Binding Path=CompanyName}" Header="First Name" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=ContactName}" Header="Last Name" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=ContactTitle}" Header="Title" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=Address}" Header="Address" IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Customer Orders">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="35" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Grid.Row="0" HorizontalAlignment="Left" Content="Customer" Margin="10,3" />
<ComboBox x:Name="comboCustomer" Grid.Row="0" Height="22" Width="150" HorizontalAlignment="Left" Margin="75,0"
ItemsSource="{Binding Customers}" DisplayMemberPath="Item2" SelectedValuePath="Item1" SelectedIndex="0">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged" >
<i:InvokeCommandAction Command="{Binding ComboCustomerChangedCommand}"
CommandParameter="{Binding ElementName=comboCustomer, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
<DataGrid Grid.Row="1" ItemsSource="{Binding Path=CustomerOrdersData}" AutoGenerateColumns="False"
AlternatingRowBackground="LightSteelBlue">
<DataGrid.Columns>
<DataGridTextColumn Width="*" Binding="{Binding Path=OrderDate, StringFormat={}{0:dd-MM-yyyy}}" Header="Order Date" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=ProductName}" Header="Product Name" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=Quantity}" Header="Quatity" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=Price, StringFormat={}{0:C}}" Header="Price" IsReadOnly="True" />
<DataGridTextColumn Width="*" Binding="{Binding Path=TotalCost, StringFormat={}{0:C}}" Header="Total Cost" IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</TabItem>
</TabControl>
<Rectangle x:Name="rectBusy" Grid.Column="0" Grid.Row="1" Grid.RowSpan="10" Grid.ColumnSpan="3" Margin="10,33,10,10"
Visibility="{Binding BusyVisibility, Mode=OneWay, UpdateSourceTrigger=PropertyChanged, FallbackValue=Visible}">
<Rectangle.Fill>
<VisualBrush>
<VisualBrush.Visual>
<StackPanel>
<StackPanel.Background>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" Opacity="0.75">
<GradientStop Color="LightGray" Offset="0.0" />
<GradientStop Color="Gray" Offset="1.0" />
</LinearGradientBrush>
</StackPanel.Background>
<TextBlock FontSize="25"/>
<TextBlock FontSize="25" FontWeight="Medium" FontFamily="Verdana" Foreground="DarkBlue" Text=" Lazy Loading "/>
<TextBlock FontSize="25"/>
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</Window>
The data binding between the View and ViewModel is achieved with the following declaration in the XAML file:
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
The local namespace
being declared in the header in the following way:
xmlns:local="clr-namespace:Dependency.Injection.WPFHost
This mechanism ensures that the bindings in the XAML file refer to properties in the class MainViewModel
. A typical binding declaration is shown below:
Visibility="{Binding BusyVisibility, Mode=OneWay, UpdateSourceTrigger=PropertyChanged, FallbackValue=Visible}"
The Visibility
property is bound to a property in the MainViewModel
called BusyVisibility
, the mode is only one way, and the view is updated when the BusyVisibility
property changes, and the RaisePropertyChanged
method is called.
As you may know, WPF does not support wiring control events to commands as standard, however this can be achieved with the use of the System.Windows.Interactivity
namespace which can be added as a reference. The namespace has to be declared in the XAML file as follows:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
The label 'i
' can now be referenced in the following way to wire an event to a command:
<ComboBox x:Name="comboDataSource" Grid.Column="0" Grid.Row="0" Width="100" Height="22" HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="30,32" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged" >
<i:InvokeCommandAction Command="{Binding ComboDatasourceChangedCommand}"
CommandParameter="{Binding ElementName=comboDataSource, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ComboBoxItem Content="Mocked Data" Selector.IsSelected="True" />
<ComboBoxItem Content="Live Data" />
</ComboBox>
The ComboBox
above SelectionChanged
event wired to a command named ComboDatasourceChangedCommand
, again inside the MainViewModel
class.
The application operates by loading into the view (MainWindow.xaml) data when it is needed, this is achieved by subscribing to the TabControl selection changed event, as shown below:
private void GetTabSelectionChangedCommandExecute(Object parameter)
{
if (parameter == null) return;
TabItem tabItem = (TabItem)parameter;
if (tabItem.Header.ToString() == "Customers")
{
InvokeMethodThreaded(new Action(() =>
{
CustomerData = NorthwindData.CustomerData.Value.ToObservableCollection();
}));
}
if (tabItem.Header.ToString() == "Customer Orders")
{
InvokeMethodThreaded(new Action(() =>
{
Customers = DataOperations.GetCustomers(NorthwindData).ToObservableCollection();
CustomerOrdersData = new ObservableCollection<OrdersData>();
}));
}
if (tabItem.Header.ToString() == "Employees")
{
InvokeMethodThreaded(new Action(() =>
{
EmployeeData = NorthwindData.EmployeeData.Value.ToObservableCollection();
}));
}
}
When the 'Customers
' and 'Employees
' tabs are selected, the properties CustomerData
and EmployeeData
are populated with the appropriate data from the NorthwindData
object. It is important to note that there is a .Value
property used, this is where the lazy loading is initiated and the data fetched from the database, or from the mocked data source. The 'Customer Orders
' tab is shown below:
In the case of the 'Customer Orders
' tab, the data is derived in a different way from the DataOperations
class, shown below:
public static ObservableCollection<OrdersData> GetCustomerOrders(Northwind northWindData, String customerID)
{
try
{
List<Order> customerOrders = (from cd in northWindData.CustomerData.Value
where cd.CustomerID == customerID
select cd).FirstOrDefault().Orders.ToList();
List<OrdersData> ordersData = new List<OrdersData>();
customerOrders.ForEach(x => ordersData.Add(
new OrdersData()
{
OrderDate = x.OrderDate,
ProductName = x.Order_Details.First().Product.ProductName,
Price = x.Order_Details.First().UnitPrice,
Quantity = x.Order_Details.First().Quantity,
TotalCost = x.Order_Details.First().UnitPrice *
x.Order_Details.First().Quantity
}));
return ordersData.ToObservableCollection();
}
catch (Exception ex)
{
return null;
}
}
A data collection of orders is enumerated from the customer selected in the ComboBox
on that tab.
Note: Data only appears in the DataGrid
when the 'Live Data' mode is selected. This is because there orders data is not being populated in the mocked data structure.
Each time the lazy loading of the data is processing, a rectangle appears over the forms tab control to prevent the user from initiating any more operations, as shown below:
Each time the 'Data Source' ComboBox
selection is changed, the following code block is executed:
private void GetComboDatasourceChangedCommandExecute(Object parameter)
{
ComboBoxItem comboBox = (ComboBoxItem)parameter;
String selection = comboBox.Content.ToString();
if (selection == "Mocked Data")
NorthwindData = container.Resolve<IData>("MockedData").Data;
if (selection == "Live Data")
{
InvokeMethodThreaded(new Action(() =>
{
NorthwindData = container.Resolve<IData>("DbData").Data;
}));
}
TabSelectedIndex = -1;
}
As can be seen, the NorthwindData
class is set to the correct data source by resolving the appropriate named configuration on the container.
The InvokeMethodThreaded
method is used to send the data operations to a separate thread, this prevents the UI thread from being blocked and freezing the application. The InvokeMethodThreaded
method is shown below:
private void UISetBusy()
{
BusyVisibility = Visibility.Visible;
}
private void UIClearBusy()
{
BusyVisibility = Visibility.Hidden;
}
private void InvokeMethodThreaded(Action actionToExecute)
{
Thread t = new Thread(delegate()
{
UISetBusy();
actionToExecute();
UIClearBusy();
});
t.Start();
}
The BusyVisibility
property enables the 'Lazy Loading' rectangle described earlier.
Instructions for Creating the Database
In order to create the Northwind
database on your local SQL Server instance, Visual Studio will have to be started in 'Administrator' mode, as shown below:
When the project has loaded, right click mouse mouse on the database project, and select 'Publish...'. The following dialog will then appear:
Next, press 'Load Profile' and select the XML file inside the database project. This loads the config as shown:
Next, select 'Edit' to edit the target database connection, the following dialog appears:
Next, change the server name to the correct one for your database server, and press OK.
Note: Do not press 'Test Connection' as it will fail since the database does not exist yet.
Finally, press 'Publish' and the database should be created, If the process is successful, the 'Data Tools Operations' view should be as shown below:
The last thing to do is to change the connection string in the App.config of the WPF host application. The data source needs to be changed to the name of your SQL Server instance as shown:
Now the application shown run in 'Live Mode' as well as 'Mocked Data' mode.
Points of Interest
This project may seem like an unusual programming pattern, with the use of lazy loading, but as Entity Framework has lazy loading enabled by default, it seemed like an interesting study. One advantage I did find was the data operations functionality (database queries) could be taken away from the database projects, and brought into the host application or another separate class library.
There is quite a number of class libraries, some with little code in them. This was done to give a scalable project structure which could be used for bigger project types.
As you may have noticed, there is a file called 'Data Demo Trace.xml'. This is taken from SQL Server Profiler. This was a study I did while using the application to see when the data was being requested from the database. The trace showed me that the data was only being requested from the database when it was being loaded into the forms, which is what I had expected.
History