Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / SQL

C# WPF Dependency Injection Data Demo

4.96/5 (19 votes)
21 Mar 2014CPOL9 min read 58.1K   1.4K  
Dependency Injection Data Demo with Lazy Loading and Data Mocking

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:

Solution

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:

DB Structure

The diagram above represents the database exactly. One of the classes created is shown below:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

Host

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:

C#
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:

C#
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:

C#
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:

XML
 <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:

XML
<Window.DataContext>
    <local:MainViewModel/>
</Window.DataContext>

The local namespace being declared in the header in the following way:

XML
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:

XML
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:

XML
<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:

C#
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:

tab

In the case of the 'Customer Orders' tab, the data is derived in a different way from the DataOperations class, shown below:

C#
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:

lazy loading

Each time the 'Data Source' ComboBox selection is changed, the following code block is executed:

C#
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:

C#
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:

admin

When the project has loaded, right click mouse mouse on the database project, and select 'Publish...'. The following dialog will then appear:

dialog

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:

edit

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:

success

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:

config

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

  • Version 1.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)