Click here to Skip to main content
Click here to Skip to main content

Layers Pattern in Practice

, 23 Apr 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Layers Pattern via a WPF project.

Contents

Introduction

The main task of this article is to present a generic application's lifecycle, with problems and solutions to common issues which programmers face on a daily basis. Once you start building an application, you ought to take into consideration lots of things, mainly from different theoretical areas of programming. Of course, as an architect of a software product, you might bump into different problems, mainly related to understanding of the entire complexity of the project, at the very beginning of the specification design. The aim of the text written below is to highlight key points which one needs to take into account at the start of the application conception. Herein, a simple WPF software product will be presented as an example, starting from its specification design, going through the development of a 3-tiered class hierarchy (User interface design - GUI; Business Logic - further noted as BL; and Data Access Layer - DAL), and ending up with a setup project and post-release debugging. One can argue that "programming is an art" and there is no need to define generic rules beforehand, constraining the architect's mind. Indeed it is so, and the article will actually try to show solutions to common problems in Windows application design, leaving the choice of the lifecycle methodology to the architect.

Background

"Where architecture tells what happens, implementer tells how it is made to happen" Blaauw

The tutorial written below will try to encompass both the architect's and the implementer's mind by showing what and how problems are solved together. The software example which is going to be presented is a BillsManager. The application's ultimate goal is to allow a client to manage his bills (simple and straightforward).

Technology used

I decided to build BillsManager as a Windows application based upon the rising in popularity Windows Presentation Foundation technology. I find it very prominent and really a step forward in Windows based development. The Data Access Layer will be constructed around XML files which will store the user's bills. So, as it can be inferred, the minimal requirement is .NET 3.5 SP.1.

Conceptual integrity

"Conceptual integrity is the most important consideration in system design" Frederick Brooks

Indeed, as practice shows, in order to successfully build any software, one should clearly define written specifications (a necessary tool, although not a sufficient one). From theory to practice - below is a list of the main objectives which BillsManager is going to meet.

Objectives

  • Show how much money a person needs to spend this month
  • Archive the payments
  • Show the nearest deadline
  • Chart the expenditures
  • Chart the expenditures by an interval of time
  • Show information about every bill
  • Give the possibility to export the archive in a file for backup
  • Assume that the target audience is not English-speaking

BillsManager will try to accomplish these fairly straightforward objectives. Although one can argue that these specifications are not enough to build a successful application, I will leave them as they are in order, not to increase the complexity of the example on which all the explanatory part is built upon.

3-tier application

"Software architecture encompasses the set of significant decisions about the organization of a software system, including the selection of the structural elements and their interfaces by which the system is composed; behavior as specified in the collaboration among those elements; composition of these structural and behavioral elements into larger subsystems; and an architectural style that guides this organization. Software architecture also involves functionality, usability, resilience, performance, reuse, comprehensibility, economic and technology constraints, tradeoffs, and aesthetic concerns" P.Kruchten, G. Booch, K. Bittner, R.Reitman

As it is stated in the above self-descriptive paragraph, while designing the application, the software architecture team should have in mind a strong view about how the requirements will be projected into the real environment, and how this environment can be as generic as it is possible. Separation of interfaces, business logic, and data access layer is a very important and also common need in software development. Essentially, nowadays, reusability plays a crucial role within the Object Oriented paradigm. The BillsManager application will implement the 3-layered structure using the Layers Pattern guidelines. Figure 1 below shows a general view of the systematization which is going to be used.

A 3-layered pattern is used in order to build better organized software parts. It provides a mechanism of defining reusable business components, alongside with deployment flexibility and smart resource connection management. All the components which are responsible for data visualization (e.g. DataGrid) will be placed in the Presentation Layer. All business logic rules will be encapsulated in business components within the BL layer. Finally, all data related code needs to be defined within the Data Access Layer. Frequently, the DAL layer is responsible for database access. In the example provided in this article, the database will be an XML file (but that does not make a difference as long as the GUI and the BL are not aware of the underlying data source). A more advanced explanation of this pattern can be found in Enterprise Solution Patterns using Microsoft .NET.

Data Access Layer

In order to have a flexible architectural component, I will start with the contract (interface) which each DAL connection manager should implement in order to meet the requirements of the Domain Layer. Following is the code for the IDalBillsManager interface.

/// <summary>
/// Interface for Bills' data source manager
/// </summary>
public interface IDalBillsManager
{
    /// <summary>
    /// Read bills
    /// </summary>
    Bills Read(DateTime fromDate, DateTime toDate);
    /// <summary>
    /// Read a bill from datasource by its ID
    /// </summary>
    Bill ReadById(Guid id);
    /// <summary>
    /// Insert bills into the datasource
    /// </summary>
    void Insert(Bills bill);
    /// <summary>
    /// Delete a bill from datasource
    /// </summary>
    int Delete(Guid guid);
    /// <summary>
    /// Delete a enumeration of bills from the datasource
    /// </summary>
    int Delete(IEnumerable<Guid> guids);
    /// <summary>
    /// Update the bill from database
    /// </summary>
    int Update(Guid billId, Bill newBill);
    /// <summary>
    /// Set settings for DAL Provider
    /// </summary>
    object[] Settings
    {
        get;
        set;
    }
}

As it can be seen from the methods which need to be implemented, they define a very general contract which can be summarized in the following responsibilities:

  • Read bills from the data source (can be any data source: XML, Excel, SQL Server, network resource, etc).
  • Delete bills from the data source.
  • Update bills in the data source.
  • Insert bills in the data source.
  • Get or set the settings for the data source (e.g., connection string, path to archive file, etc.).

It is important to point out that the contract should relinquish the following rule within [IN] and [OUT] parameters. It should have:

  • Most general parameters at the input [IN] (E.g., IEnumerable<guid> interface)
  • Most specific return types [OUT] (E.g., Bills)

Subsequent to the interface definition, it is valuable to mention that the Business Logic programmer will access methods of the DAL Manager only through an interface instance.

private IDalBillsManager _dalManager = XmlDalBillsManager.GetInstance();

If in a future development cycle, you choose to switch the DAL Manager to another version or to an entirely new library, you will need to change only one line of code, which is the assignment operation written above. After doing this, the entire business logic library will work as it is supposed to, without requiring any other changes in the code. The responsibilities, defined in the IDalBillsManager interface will be held by the Data Access Manager. In our case, it will be an XML-based DAL Manager, meaning that all the bills information will be stored in an XML archive file. Below, you can see the XML schema on which our archive will relinquish (this schema is located in BillSchema.xsd under the BillEntityLib project folder). It is very important to note that our XML will use strongly established rules of composition.

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="billschema"
    targetNamespace="http://tempuri.org/billschema.xsd"
    elementFormDefault="qualified"
    xmlns="http://tempuri.org/billschema.xsd"
    xmlns:mstns="http://tempuri.org/billschema.xsd"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Bills">
    <xs:complexType>
      <xs:choice minOccurs="1" maxOccurs="unbounded">
        <xs:element name="Bill" type="Bill"/>
      </xs:choice>
    </xs:complexType>
  </xs:element>
  <xs:complexType name="Bill">
    <xs:sequence>
      <xs:element name="Name" type="xs:string"
                  minOccurs="1" maxOccurs="1"/>
      <xs:element name="DueDate" type="xs:dateTime"
                  minOccurs="1" maxOccurs="1"/>
      <xs:element name="Amount" type="xs:decimal"
                  minOccurs="1" maxOccurs="1"/>
      <xs:element name="AddedOn" type="xs:dateTime"
                  minOccurs="1" maxOccurs="1"/>
      <xs:element name="Status" type="xs:string"
                  minOccurs="1" maxOccurs="1"/>
    </xs:sequence>
    <xs:attribute name="ID" form="unqualified" type="xs:string"/>
  </xs:complexType>
</xs:schema>

The XSD file describes the content that is allowed in an XML document in order for the last one to be considered valid (i.e., within the defined constraints). If you are not comfortable with XML schemas and their rules, please consult any available material on this topic. Our XML schema is formed from the following elements:

  • Bills (Root element)
    • Bill (Will represent every bill)
      • Name (Name of the bill)
      • DueDate (Bill's due date)
      • Amount (Bill's amount)
      • AddedOn (When was the bill added on)
      • Status (Bill's status)

Our XML-based Data Access Manager will be a singleton object (meaning that where will be only one instance of the object in the entire application lifecycle). If you are not familiar with singletons, please consult the Singleton pattern article for a better explanation. The following figure presents the class diagram for the XmlDalBillsManager class. (Please notice that it implements the IDalBillsManager interface.)

Consequently, our XML based manager should support thread-safe operations (so that the archive does not get corrupt if multiple threads attempt to write to the database at the same time - which could result in data loss or unpredictable behavior). Thus, the user is allowed to call the operations Insert, Read, Update, and Delete without a clue of how the archive is protected from corruption. As it is not difficult to observe, here we can implement the Readers/Writers thread algorithm in order to solve the above mentioned problem. Herein, the manager will implement the following policy:

  • If somebody reads the data from the archive, other readers are allowed to perform the same operation (access for writers is prohibited).
  • If somebody writes the data into archive, no other thread (read or write) can perform any operation with the above mentioned data source.

For a more detailed review, please consult any available material related to the Readers/Writers threading algorithm.

Entity Objects

Having the XML schema defined, we can now generate entity classes. These are the classes which do not hold any responsibility (do not have any methods). They are just binary representations of the data from the XML. The Bills and Bill classes will perform only the storage function within the Business Logic Domain (they will not have any additional methods except getters/setters for their properties). There are different approaches within the Layers pattern regardless of the entity and business logic elements. You can define the business logic methods (Insert, Read, Delete, Update) in the same classes in the entity library (we'll see later what methods will actually be implemented in the business domain). The others can be separated in two different classes. I've used the following separation of business components:

  • Entity classes
  • Manager class

Don't worry if you do not understand the entire concept; you'll get it once you take a look at the actual implementation. I've decided to perform the separation, because it is more natural to have exactly the same copy of a Bill item in the computer memory as it is in the data source (no matter what data source: XML, text, Excel, relational database, etc.). At the same time, these entity objects will actually be serializable. This feature will allow any .NET programmer to extend the program functionality by adding a new layer within the Business Logic and the Data Layer and pass objects from the Business Domain to the Data Domain in serialized state. Serialization allows to save an object's state (Bills and Bill) into any stream (memory, network, file, etc.), and pass it within the Application Domain or Remoting services.

From BillSchema.xsd, we can see that there will be only two entity classes: Bills and Bill. .NET provides an easy mechanism for generating .cs files from .xsd schemas. This mechanism is achieved through the XML Schema Definition tool. One can access xsd.exe through Visual Studio's command prompt or by opening the command line and navigating to c:\Program Files\Microsoft Visual Studio 9.0\VC>. The command which needs to be applied is xsd [name of xml schema].xsd /c. By applying this command, the XSD tool will generate partial classes which correspond to the schema definition.

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.4926
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System.Xml.Serialization;

// 
// This source code was auto-generated by xsd, Version=2.0.50727.3038.
// 


/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.303")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, 
        Namespace="http://tempuri.org/billschema.xsd")]
[System.Xml.Serialization.XmlRootAttribute(
        Namespace="http://tempuri.org/billschema.xsd", 
        IsNullable=false)]
public partial class Bills { /*Bills collection from the datasource*/
    
    private Bill[] itemsField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("Bill")]
    public Bill[] Items {
        get {
            return this.itemsField;
        }
        set {
            this.itemsField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.3038")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(
  Namespace="http://tempuri.org/billschema.xsd")]
public partial class Bill {
/*Each bill from the datasource, has exactly the same properties*/
    
    private string nameField;
    
    private System.DateTime dueDateField;
    
    private decimal amountField;
    
    private System.DateTime addedOnField;
    
    private string statusField;
    
    private string idField;
    
    /// <remarks/>
    public string Name {
        get {
            return this.nameField;
        }
        set {
            this.nameField = value;
        }
    }
    
    /// <remarks/>
    public System.DateTime DueDate {
        get {
            return this.dueDateField;
        }
        set {
            this.dueDateField = value;
        }
    }
    
    /// <remarks/>
    public decimal Amount {
        get {
            return this.amountField;
        }
        set {
            this.amountField = value;
        }
    }
    
    /// <remarks/>
    public System.DateTime AddedOn {
        get {
            return this.addedOnField;
        }
        set {
            this.addedOnField = value;
        }
    }
    
    /// <remarks/>
    public string Status {
        get {
            return this.statusField;
        }
        set {
            this.statusField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string ID {
        get {
            return this.idField;
        }
        set {
            this.idField = value;
        }
    }
}

Following is the class diagram for the Entity objects. Please note that I've added some additional class members to those which were generated. (I will explain later why there was a need to implement the INotifyPropertyChanged interface by the Bill class).

Business Logic Layer

I will now discuss the Business Logic responsibilities and how they were materialized in the actual code. As in the case of the Data Domain, I've decided to define an interface IBillsManager which is going to deliver the contract to all the classes which will implement that interface. Here is the definition of the IBillsManager interface:

/// <summary>
/// Interface for Bill manager business logic
/// </summary>
public interface IBillsManager
{
    /// <summary>
    /// Read from data source
    /// </summary>
    Bills Read(DateTime fromDate, DateTime toDate);

    /// <summary>
    /// Reads a bill by its ID
    /// </summary>
    Bill ReadById(Guid id);

    /// <summary>
    /// Insert a bill into the database
    /// </summary>
    void Insert(Bills bill);

    /// <summary>
    /// Delete a bill from database
    /// </summary>
    int Delete(Guid bill);

    /// <summary>
    /// Delete a list of bills with the specified guids
    /// </summary>
    int Delete(IEnumerable<Guid> guids);

    /// <summary>
    /// Update a bill in the database
    /// </summary>
    int Update(Guid billId, Bill newBill);

    /// <summary>
    /// Settings
    /// </summary>
    object[] Settings
    {
        get;
        set;
    }
}

You can see that the aforementioned interface defines exactly the same methods as the Data Layer interface IDalBillsManager. Yes, indeed the methods are the same because the actual responsibilities are also the same. The only difference is that every manager (either from the Business Domain or from the Data Layer) is responsible for different logical operations on the data which passes through. As an example, see the following figure:

As can be seen, even though the method signature for both the Business and Data Domain is the same, internally, they are responsible for different logical interactions within the objects. In the above example, the Business Domain insertion operation is responsible for:

  • Validation
  • Sending the request to the Data Layer
  • Checking whether the Insert operation occurred or not
  • Throwing an exception if appropriate

At the same time, the Data Layer is responsible for:

  • The actual insert operation into the data source

One can argue that there was no need to define two different interfaces with the same signature for the Business and Data Domains; instead, I should have used the same interface for both. I definitely consider this argument a mistake, because the client programmer would be able to cast Business and Data Logic objects into the same interface, without a clue of whether he accesses methods from one or the other domain. This approach breaks the entire philosophy of layer separation and should never be used.

Presentation Layer

The last but not the least layer in the pattern is the Presentation Layer. It is actually responsible for the interaction between the user and the machine. There are plenty of topics which one might find useful while developing a GUI (Graphical User Interface). If you are building a WPF application, you might find it useful to check Sacha Barber's articles here at CodeProject. Anyway, I will cover several of them which I think are the most generic ones. We'll start with the DataGrid, a component which is responsible for presenting the data to the end user. Then we'll move to a more specific problem as charting the data and localizing string resources.

Datagrid

By default, .NET 3.5 does not ship a WPF datagrid component. There are different viewpoints whether it is good or not. I will just point out that I decided (like many other developers) to use the WPFToolkit which can be freely downloaded from the CodePlex site (click here), in order to have a built-in DataGrid with all the bells and whistles. Personally, I find those components really useful, so I really encourage everybody to explore them.

A core action one needs to define in a data grid is its binding to a data source. Binding is the concept of establishing a connection between a UI (user interface) component and the business logic. There is a very nice article on the MSDN site which explains how binding works in WPF components (click here). Typically, each binding has these four components:

  • a binding target object
  • a target property
  • a binding source
  • a path to the value in the binding source to use

For example, if you want to bind the content of a TextBox to the Name property of an Employee object, your target object is the TextBox, the target property is the Text property, the value to use is Name, and the source object is the Employee object. Here is the XAML code which declares and defines the DataGrid for BillsManager with the necessary binding.

<WPFToolkit:DataGrid x:Uid="_dgBills" 
        Style="{StaticResource DataGrid}" 
        ItemContainerStyle="{StaticResource ItemContStyle}" 
        x:Name="_dgBills" Margin="8,42.96,8,167.08" 
        IsReadOnly="False" AutoGenerateColumns="False" 
        CanUserAddRows="False">
    <WPFToolkit:DataGrid.Columns>
        <WPFToolkit:DataGridTextColumn Header="Bill" 
            Width="100" Binding="{Binding Name}"/>
        <WPFToolkit:DataGridTextColumn Header="Due Date" 
            Width="100" 
            Binding="{Binding DueDate, Converter={StaticResource DateConverter}}"/>
        <WPFToolkit:DataGridTextColumn Header="Amount" 
            Width="75" Binding="{Binding Amount}"/>
        <WPFToolkit:DataGridTextColumn Header="Added On" 
           Width="100" 
           Binding="{Binding AddedOn, Converter={StaticResource DateConverter}}"/>
        <WPFToolkit:DataGridComboBoxColumn Header="Status" 
           Width="75" 
           SelectedItemBinding="{Binding BillStatus}" 
           ItemsSource="{Binding Source={StaticResource myEnum}}"/>
    </WPFToolkit:DataGrid.Columns>
</WPFToolkit:DataGrid>

As can be seen, each DataGrid column is bind to a specific property from our Entity's library Bill class.

  • Name - Binding="{Binding Name}"
  • DueDate - Binding DueDate, Converter={StaticResource DateConverter}
  • Amount - Binding="{Binding Amount}"
  • AddedOn - Binding="{Binding AddedOn, Converter={StaticResource DateConverter}}"
  • Status - SelectedItemBinding="{Binding BillStatus}" ItemsSource="{Binding Source={StaticResource myEnum}}"

It is worth mentioning that the Converter item from the DataGrid column definition will perform the formatting operation of DataTime values (it will show the date in short format DD.MM.YY). Shown below is the actual code for the converter item:

[ValueConversion(typeof(DateTime), typeof(String))]
public class DateConverter : IValueConverter
{
    /*
     * Convert each data item from the data grid into short format DD/MM/YY 
     */
    public object Convert(object value, Type targetType, 
                  object parameter, CultureInfo culture)
    {
        DateTime date = (DateTime)value;
        return date.ToShortDateString();
        /*Show the date in the datagrid in Short format*/
    }
    /*Convert back*/
    public object ConvertBack(object value, Type targetType, 
           object parameter, CultureInfo culture)
    {
        string strValue = value as string;
        DateTime resultDateTime;
        if (DateTime.TryParse(strValue, out resultDateTime))
        {
            return resultDateTime;
        }
        return DependencyProperty.UnsetValue;
    }
}

The code above binds the data to Bills, following this logic:

"To bind the datagrid to data, set the ItemsSource property to an IEnumerable implementation. Each row in the datagrid is bound to an object in the data source, and each column in the datagrid is bound to a property of the data object. In order for the DataGrid user interface to update automatically when items are added to or removed from the source data, the DataGrid must be bound to a collection that implements INotifyCollectionChanged, such as an ObservableCollection<T>. In order to automatically reflect property changes, the objects in the source collection must implement the INotifyPropertyChanged interface". Click here for more details.

Bills from the data source are represented in the computer memory via the Bills class. Afterwards, these items are embedded in the ObservableCollection<Bill> collection. In this way, we can add, remove, edit elements of underlying list of bills without actually taking care of updating the user interface to reflect the changes (.NET will perform this automatically). Click here for more details.

As written above, we'll bind the data grid item source property to ObservableCollection<Bill>.

_collection = new ObservableCollection<Bill>(bills.Items);
//define the ObservableCollection<T> class

this._datagrid.ItemsSource = _collection;
//bind the data to the datasource

Disconnected Data Environment

I will now discuss the disconnected data environment approach which was implemented in our BillsManager application. This environment gives us an answer to how updates, deletes, and inserts of bills objects to the data grid control are reflected in the actual data source. Generally, there are two basic mechanisms:

  • Connected Data Environment - once an entity (Bill) is modified in the Business Domain, the actual modification is also performed in the Data Domain, meaning that a strong bidirectional relationship is defined within both layers.
  • Disconnected Data Environment - entities (Bills) which are modified in the Business Domain are marked as updated, but no modifications are performed in the Data Domain. Once a crucial action is performed within the application (e.g., the application is closed), all the fields which are marked as updated are modified in the Data Domain according to their new values.

This second approach has a lot more advantages in its use. Mostly, these advantages relate to a lot more flexibility. Imagine a user changing a bill's status from Unpaid to Paid. Also, he might want to modify the Amount field of the same bill. In the connected data environment, these operations will result in the following method calls:

  • Change bill's status property from Unpaid to Paid
  • Change bill's amount property

In a disconnected data environment, the aforementioned operation will result in the following:

  • Mark the bill as Updated (please note, this operation will not call any method from the Data Domain).
  • Once the Commit operation is called, update both bills' properties at once.

The above mentioned mechanism was possible through the usage of the Publish/Subscribe event model. Every bill entity allows any client to subscribe to its PropertyChangedEventHandler PropertyChanged event. Once you've subscribed to this event, you'll receive a notification whether a bill's property was changed.

Styling the grid

The style of the grid is located in the resource dictionary (DataGrid.Generic.xaml). The interesting thing about the style of the grid is conditional formatting. Once a bill's due date is overdue and a bill's status is Unpaid, the data row changes its color from transparent to yellow. This is done through conditional formatting. A developer can set the ItemContainerStyle property of the grid to an element which will dynamically change the background of the data row according to some logics defined in the code behind. So we'll define a new resource dictionary element and call it ItemContStyle:

<Style x:Key="ItemContStyle" TargetType="{x:Type WPFToolkit:DataGridRow}">
    <Style.Resources>
        <billPayManager:DataRowBackgroundConverter x:Key="BackgroundConverter" />
    </Style.Resources>
    <Setter Property="WPFToolkit:DataGrid.Background">
        <Setter.Value>
            <MultiBinding Converter="{StaticResource BackgroundConverter}">
                <MultiBinding.Bindings>
                    <Binding Path="DueDate"/>
                    <Binding Path="Status"/>
                </MultiBinding.Bindings>
            </MultiBinding>
        </Setter.Value>
    </Setter>
</Style>

The DataRowBackgroundConverter class's Convert method will be invoked. According to the data which is set in the data row, it will return Yellow or no color.

[ValueConversion(typeof(object), typeof(int))]
public class DataRowBackgroundConverter : IMultiValueConverter
{
    #region IMultiValueConverter Members
    /*
     * Check each element for the corresponding background color
     */
    public object Convert(object[] values, Type targetType, 
           object parameter, CultureInfo culture)
    {
        if (values[0] is DateTime)
        {
            DateTime dueDate = (DateTime)values[0];
            string status = values[1] as string;
            if (dueDate < DateTime.Now && 
                BillHelper.Convert(status) == BillStatus.Unpaid)
            {
                return new SolidColorBrush(Colors.Yellow);
                /*Return Yellow if DueDate is overdue and BillStatus is unpaid*/
            }
        }
        return null;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, 
           object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

As you can see, WPF offers great flexibility in terms of data binding and data formatting.

Chart

Charting is a topic which is definitely very often discussed on programming forums. Many people find it challenging, because lots of things need to be taken into consideration. I decided to use a third party charting tool rather than the one defined in the WPFToolkit. It can be freely downloaded from the Visifire site. It is a framework which provides Open Source data visualization components. I find it very nice and easy to use. The things which I would like to point out are, that the chart itself draws the data according to a set of different parameters, which can be set in many ways (DataSeries - series of data points which actually represent the pair X, Y, where F(x) = Y; Axis - pie, line, bar, 2D, 3D, etc., Title - the actual title text). In order to have an extensible mechanism of drawing the chart using different Axis, I decided to use the Strategy pattern. The strategy is actually the way how axis will be represented (this is done in order to have three different types of chart: Line, Bar, Pie). The following figure presents the class diagram for these items:

Consequently, chart properties are set according to one of the methods of drawing it:

//chartType object of type ICharting
public static void SetChartValuesAsync(Chart chart, IEnumerable<bill> list, 
       ICharting chartType, IntervalTypes interval,
       ChartValueTypes chartValueType, bool scrollingEnabled, bool erasePrevious)
{
    if (chart != null)
    {
        chart.Dispatcher.BeginInvoke(new Action(delegate()
        {
            if (erasePrevious)
            {
                chart.AxesX.Clear();
                chart.AxesY.Clear();
                chart.Titles.Clear();
                chart.Series.Clear();
            }

            // Add title to Titles collection
            chart.Titles.Add(chartType.GetTitle(list.First().Name));
            /*Set Axis via interface instance*/
            chart.AxesX.Add(chartType.GetAxis(interval, list));
            chart.ScrollingEnabled = scrollingEnabled;
            chart.Series.Add(chartType.GetDataSeries(list, chartValueType));
        }), null);
    }
}

As we can see, the mechanism is maintainable and extensible, because even if we add new types of charts in the following versions of the application, we only need to implement the corresponding ICharting interface and pass an object of its type to the SetChartValuesAsync method. In the figure below, we can see an example of the aforementioned settings adjustment:

Localization

Localization is the process of preparing an application to run in multiple locations. The .NET Framework provides very thorough support for localizing all types of client applications. Culture information and manipulation in the .NET Framework is exposed through an instance of the CultureInfo class, which can be used to read culture settings for a given locale, as well as setting a specific locale in an application. Every thread maintains a set of culture information stored as CultureInfo settings in the CurrentCulture and CurrentUICulture properties.

WPF applications have two possibilities to set different locale statuses:

  • LocBaml tool
  • Resx approach

In my application, I'll be using the Resx resource files approach (mainly because it is a classical way of setting different locales in Windows Applications). Resx files are XML based files which are compiled into resource.dll libraries and can be loaded into the application via the ResourceManager class. The following snippet shows the code which performs the actual load:

CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
string culture = ConfigurationManager.AppSettings["Culture"];
if (!String.IsNullOrEmpty(culture))
   if(culture != "Default")
       currentCulture = new CultureInfo(culture);
ResourceManager resx = new ResourceManager(
  "BillPayManager.Properties.Resources", typeof(App).Assembly);

The currentCulture object will hold a thread's culture description. The resx object will allow us to query the .dll file with the actual cultural specific information. The default culture is read by the BillsManager application from the application configuration file (Key = "Culture"). It is important to mention that if the App.config file doesn't explicitly set culture information, then the en-US culture will be used. Here is a query example:

this._lTotalAmount.Content = resx.GetString("TotalAmount", currentCulture);
this._lNearestDeadline.Content=resx.GetString("NearestDeadline",currentCulture);

The resource files are stored within the folders with the same name as cultural info (en-US, ro-RO). Next, we can visualize how MainAssembly gets the resources from the corresponding directories (see figure).

In order to change the culture, please change the BillPayManager.exe.config file or change it via the Miscellaneous tab:

<appSettings>
    <add key="Culture" value="ro-RO"/>
</appSettings>

Be attentive while changing the Culture key. You can explicitly set only cultures defined in the corresponding sub folders (en-US or ro-RO).

Post release debugging

The importance of post release application debugging cannot be underestimated. Even though testing of the application can end up in a very small number of undetected bugs, once installed on user machines, the application behavior cannot be predicted. This might happen because of several reasons: user has no admin privileges, the OS internal configuration is different of expected ones, or your application demands read/write operations to critical regions as the root directory of the system drive (usually C:\), the HKLM (local machine) key in the Registry, etc. Yet, developers have a mechanism which allows them to figure out problems which appear on the client-side machine, without the need to literally come to the client's office or home. This mechanism is called Tracing.

Even though we are unable to predict where and when the application is going to crash, we can assume that in some critical points, it might have problems of doing some sort of operations. These critical points can be marked as traceable. Once the application reaches these points, we ought to write the information about the current state (variables, stack trace etc.) in a file, which, if needed, can be shipped to developers in order for them to solve the problem. Having the application's log file will allow them to recall steps which caused the application to crash. This simple mechanism can save huge amounts of time and money. .NET Framework is shipped with the System.Diagnostics namespace, which has plenty of features that can help us to trace, debug, and monitor the performance of our application. The class in which we are interested in order to meet the post-installation debugging purpose is the Trace class. Next, I will post a passage from here which best describes the purpose and usage of the aforementioned class.

You can use the properties and methods in the Trace class to instrument release builds. Instrumentation allows you to monitor the health of your application running in real-life settings. Tracing helps you isolate problems and fix them without disturbing a running system. This class provides methods to display an Assert dialog box, and to emit an assertion that will always fail. This class provides write methods in the following variations: Write, WriteLine, WriteIf, and WriteLineIf. The BooleanSwitch and TraceSwitch classes provide means to dynamically control the tracing output. You can modify the values of these switches without recompiling your application. You can customize the tracing output's target by adding TraceListener instances to or removing instances from the Listeners collection. The Listeners collection is shared by both the Debug and the Trace classes; adding a trace listener to either class adds the listener to both. By default, trace output is emitted using the DefaultTraceListener class.

The Microsoft Best Practices team advices developers to use the App.config file for the adjustments of trace listeners (this is pretty obvious, as long as changes in application configuration file do not require recompilation). Anyway, I've decided to adjust the diagnostics objects in the code because of several issues. As an example, consider you want to use DelimiterTraceListener as your main listener. Once you have specified in App.config the path to the file that will actually store the output messages, you have no way of checking whether the user has write privileges to that location or not. This means that if the user doesn't have the required permissions, the application will throw an exception at startup once the CLR tries to instantiate the aforementioned objects. Because of this, the BillsManager application adjusts Trace objects in the method which is written below.

/// <summary>
/// Initialize Delimited Trace listener
/// </summary>
private void InitializeDelimitedTraceListener()
{
    bool enabled;
    string tracepath = "";
    try
    {
        enabled = Convert.ToBoolean(
          ConfigurationManager.AppSettings["BooleanSwitch"], 
          _currentCulture);
        tracepath = System.IO.Path.GetFullPath(
          ConfigurationManager.AppSettings["PathToTraceFile"]);
    }
    catch (FormatException)
    {
        enabled = false; /*Disable Boolean switch*/
    }
    this._chbTrace.IsChecked = enabled;
    string path = ProbeFilePermissions(tracepath, FileIOPermissionAccess.Write);
    /*Check if user has enough privileges to Write in the corresponding folder */     

    if (path != tracepath)
    {

        try
        {
            ModifyAppConfig("PathToTraceFile", path);
            /*Modify App.Config if another path is chosen*/
        }
        catch(ConfigurationErrorsException)
        {
            Debugger.Break();
        }
    }
    long maxSize = 1045680; /*1 MB*/
    try
    {
        maxSize = Convert.ToInt32(
          ConfigurationManager.AppSettings["SizeOfTrace"]);
          /*Max Size of trace file*/
    }
    catch (FormatException)
    {
        Debugger.Break();
    }
    finally
    {
        try
        {
            FileInfo fileInfo = new FileInfo(path);
            if(fileInfo.Length > maxSize)
                File.Delete(path);
            
        }
        catch(Exception)
        {
            Debugger.Break();
        }
    }
    DelimitedListTraceListener listener = new DelimitedListTraceListener(path)
                                              {
                                                  Delimiter = "__",
                                                  IndentSize = 4,
                                                  TraceOutputOptions = TraceOptions.DateTime
                                              };
    Trace.Listeners.Add(listener); /*Adding listener to the collection*/
    Trace.AutoFlush = true;
    this._bSwitch = new BooleanSwitch("Switch", "Switch");
    _bSwitch.Enabled = enabled;
    this.TraceSystemSettings(); /*Tracing system settings*/
}

The ProbeFilePermissions() method probes the location for FileIOPermissionAccess.Write, and if the user lacks the specified permissions, returns an alternative path to which the user has Write privileges.

Testing

Testing is one of the most unpleasant parts of developing an application. There are different approaches to the way how testing is performed (Manual, Automation testing, Unit tests). They depend mostly upon what methodology was used in software development. I prefer using Unit tests because there is a great way of visualizing the Data Flow, Exceptions, Assertions, etc. In order to write a unit test, we needs to install the NUnit framework, freely downloadable from their official website. Once installed, you can use features available in the NUnit.Framework namespace in order to test any public method. For more information about writing Unit Tests, click here.

Source code

I've written the application using Visual Studio 2008 TS. The source code is structured as follows:

  • BillManagerUninstallAction project. Custom uninstall action implemented in order to delete all the associated files (archive and trace output) once the application is uninstalled from the user's machine.
  • BillPayManager project. This is the actual WPF application. The GUI was developed using the Expression Blend 3 IDE.
  • BillsBusinessLogicLib project. The library contains all the necessary classes for Business Layer management (BL). Main class - BillsManager.
  • BillsDalLib project. The library where all XML data access related code is located. The main class for DAL is - XmlDalBillsManager.
  • BillsEntityLib project. Entity library.
  • TestBillPayManager project. Several Unit Test fixtures are written in this project. I've deleted the project from the solution configuration in order for people who don't have NUnit installed on their machines to be able to compile the solution without errors. If you want to build it, please add it to the project.
  • TestBillPayManagerManual project. Manual tests written for GUI testing. The project is also unloaded.
  • SetupBillPayManager project. Setup for BillsManager.

In order to successfully compile the project, we need additional libraries referenced in the BillPayManagement project. These libraries are:

Conclusion

During my student's life, I've always had enough books on software design. Even if each of them provides very nice analysis of software architecture, none of them speaks about issues related to practical implementation. This is due to the fact that authors are most likely to speak in abstract terms, in order not to tie themselves to a specific technology (.NET Framework in my case). This is of course great, but I've always wanted a guide which will have as an example a complete product, starting from the specification design and ending up with the setup project. The aim of this article is to speak about the main issues related to .NET Windows application design, not in terms of theory, but in terms of practice. You can argue that architecture design is a topic which differs from one product to another, and it's mostly impossible to speak about every tiny problem which you will encounter during an application's lifecycle. This is of course true, but the article's target audience is "computer science students" and not senior developers who already know all the stuff written in here. That's why I've designed a simple WPF application in order to have a generic guide for students (or uninitiated WPF developers) who would like to build these types of applications. I've tried to keep the article's size as small as possible, but somehow it extended above initial expectations. Thanks for reading.

Thanks

  • Alex Railean - http://railean.net/ for helping me writing this article.
  • Victoria - for being kind.

License

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

Share

About the Author

Ciumac Sergiu
Software Developer
Moldova (Republic Of) Moldova (Republic Of)
Interested in computer science, math, research, and everything that relates to innovation. Fan of agnostic programming, don't mind developing under any platform/framework if it explores interesting topics. In search of a better programming paradigm.
Follow on   Twitter   LinkedIn

Comments and Discussions

 
QuestionUpdates Pinmemberkiquenet.com27-Feb-13 4:10 
GeneralMy vote of 3 PinmemberMichael Agroskin13-Feb-11 11:37 
GeneralRe: My vote of 3 PinmemberCiumac Sergiu22-Feb-11 2:22 
GeneralRe: My vote of 3 Pinmemberbilo816-Oct-11 3:56 
GeneralGreat Article but a question [modified] PinmemberKhaniya1-Jan-11 2:12 
GeneralRe: Great Article but a question PinmemberCiumac Sergiu3-Jan-11 0:13 
Hi Khaniya,
You can visualize .xsd schema in BillsEntityLib, by double-clicking the BillSchema.xsd file. I was using Visual Studio 2008 while developing this project, and by default it shows me the .xsd files as xml structured documents. I think you default .xsd viewer is set to another editor. In order to change it, right click BillSchema.xsd, select Open With, and select appropriate viewer (I'm using XML Editor). Hope this helps.
Kind regards, Ciumac Sergiu
GeneralRe: Great Article but a question PinmemberKhaniya3-Jan-11 2:53 
Generalmy vote of 5 PinmemberRay Guan1-Dec-10 16:32 
GeneralRe: my vote of 5 PinmemberCiumac Sergiu2-Dec-10 1:40 
GeneralExcellent Pinmemberminakshijuware30-Nov-10 2:48 
GeneralRe: Excellent PinmemberCiumac Sergiu1-Dec-10 2:57 
GeneralMy vote of 5 Pinmemberminakshijuware30-Nov-10 2:46 
GeneralMy vote of 5 Pinmembermaq_rohit16-Aug-10 21:38 
GeneralMy vote of 5 Pinmemberkempol8-Jul-10 23:27 
GeneralRe: My vote of 5 PinmemberCiumac Sergiu9-Jul-10 3:16 
GeneralGr8 entry PinmvpMd. Marufuzzaman5-Jul-10 4:27 
GeneralRe: Gr8 entry PinmemberCiumac Sergiu5-Jul-10 10:55 
GeneralGodd article PinmemberSteppenwolfe19-May-10 10:17 
GeneralMinor problem... PinmemberPaulo Zemek27-Apr-10 7:51 
GeneralRe: Minor problem... PinmemberCiumac Sergiu27-Apr-10 8:47 
GeneralRe: Minor problem... PinmemberPaulo Zemek27-Apr-10 9:48 
GeneralRe: Minor problem... PinmemberCiumac Sergiu27-Apr-10 10:01 
QuestionNice functioanl style. Can someone do this OO now?? PinmemberLugnuts27-Apr-10 7:03 
AnswerRe: Nice functioanl style. Can someone do this OO now?? [modified] PinmemberCiumac Sergiu27-Apr-10 9:54 
GeneralRe: Nice functioanl style. Can someone do this OO now?? PinmemberLugnuts29-Apr-10 7:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.141022.1 | Last Updated 23 Apr 2010
Article Copyright 2010 by Ciumac Sergiu
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid