Follow @PDSAInc July 2012 - Posts - Paul Sheriff's Blog for the Real World

Paul Sheriff's Blog for the Real World

This blog is to share my tips and tricks garnered over 25+ years in the IT industry

Paul's Favorites

July 2012 - Posts

WPF Tree View with Multiple Levels

Earlier this year I blogged on how to use the WPF Tree View to view multiple levels. Since then I have had many requests to do the same in WPF. Luckily, the code is almost identical. Here is a blog post on using the WPF Tree View that has multiple levels.

There are many examples of the WPF Tree View that you will find on the web, however, most of them only show you how to go to two levels. What if you have more than two levels? This is where understanding exactly how the Hierarchical Data Templates works is vital. In this blog post, I am going to break down how these templates work so you can really understand what is going on underneath the hood. To start, let’s look at the typical two-level WPF Tree View that has been hard coded with the values shown below:

<TreeView>
  <TreeViewItem Header="Managers">
    <TextBlock Text="Michael" />
    <TextBlock Text="Paul" />
  </TreeViewItem>
  <TreeViewItem Header="Supervisors">
    <TextBlock Text="John" />
    <TextBlock Text="Tim" />
    <TextBlock Text="David" />
  </TreeViewItem>
</TreeView>

Figure 1 shows you how this tree view looks when you run the WPF application.

Figure 1: A hard-coded, two level Tree View.

Figure 1: A hard-coded, two level Tree View.

Next, let’s create three classes to mimic the hard-coded Tree View shown above. First, you need an Employee class and an EmployeeType class. The Employee class simply has one property called Name. The constructor is created to accept a “name” argument that you can use to set the Name property when you create an Employee object.

public class Employee
{
  public Employee(string name)
  {
    Name = name;
  }

  public string Name { get; set; }
}

Create an EmployeeType class that has one property called EmpType and contains a generic List<> collection of Employee objects. The property that holds the collection is called Employees.

public class EmployeeType
{
  public EmployeeType(string empType)
  {
    EmpType = empType;
    Employees = new List<Employee>();
  }

  public string EmpType { get; set; }
  public List<Employee> Employees { get; set; }
}

Finally we have a collection class called EmployeeTypes created using the generic List<> class. It is in the constructor for this class where you will build the collection of EmployeeTypes and fill it with Employee objects:

public class EmployeeTypes : List<EmployeeType>
{
  public EmployeeTypes()
  {
    EmployeeType type;
       
    type = new EmployeeType("Manager");
    type.Employees.Add(new Employee("Michael"));
    type.Employees.Add(new Employee("Paul"));
    this.Add(type);

    type = new EmployeeType("Project Managers");
    type.Employees.Add(new Employee("Tim"));
    type.Employees.Add(new Employee("John"));
    type.Employees.Add(new Employee("David"));
    this.Add(type);
  }
}

You now have a data hierarchy in memory (Figure 2) which is what the Tree View control expects to receive as its data source.

Figure 2: A hierachial data structure of Employee Types containing a collection of Employee objects.

Figure 2: A hierachial data structure of Employee Types containing a collection of Employee objects.

To connect up this hierarchy of data to your Tree View you create an instance of the EmployeeTypes class in XAML as shown in line 13 of Figure 3. The key assigned to this object is “empTypes”. This key is used as the source of data to the entire Tree View by setting the ItemsSource property as shown in Figure 3, Callout #1.

Figure 3: You need to start from the bottom up when laying out your templates for a Tree View.

Figure 3: You need to start from the bottom up when laying out your templates for a Tree View.

The ItemsSource property of the Tree View control is used as the data source in the Hierarchical Data Template with the key of employeeTypeTemplate. In this case there is only one Hierarchical Data Template, so any data you wish to display within that template comes from the collection of Employee Types. The TextBlock control in line 20 uses the EmpType property of the EmployeeType class. You specify the name of the Hierarchical Data Template to use in the ItemTemplate property of the Tree View (Callout #2).

For the second (and last) level of the Tree View control you use a normal <DataTemplate> with the name of employeeTemplate (line 14). The Hierarchical Data Template in lines 17-21 sets its ItemTemplate property to the key name of employeeTemplate (Line 19 connects to Line 14). The source of the data for the <DataTemplate> needs to be a property of the EmployeeTypes collection used in the Hierarchical Data Template. In this case that is the Employees property. In the Employees property there is a “Name” property of the Employee class that is used to display the employee name in the second level of the Tree View (Line 15).

What is important here is that your lowest level in your Tree View is expressed in a <DataTemplate> and should be listed first in your Resources section. The next level up in your Tree View should be a <HierarchicalDataTemplate> which has its ItemTemplate property set to the key name of the <DataTemplate> and the ItemsSource property set to the data you wish to display in the <DataTemplate>. The Tree View control should have its ItemsSource property set to the data you wish to display in the <HierarchicalDataTemplate> and its ItemTemplate property set to the key name of the <HierarchicalDataTemplate> object. It is in this way that you get the Tree View to display all levels of your hierarchical data structure.

Three Levels in a Tree View

Now let’s expand upon this concept and use three levels in our Tree View (Figure 4). This Tree View shows that you now have EmployeeTypes at the top of the tree, followed by a small set of employees that themselves manage employees. This means that the EmployeeType class has a collection of Employee objects. Each Employee class has a collection of Employee objects as well.

Figure 4: When using 3 levels in your TreeView you will have 2 Hierarchical Data Templates and 1 Data Template.

Figure 4: When using 3 levels in your TreeView you will have 2 Hierarchical Data Templates and 1 Data Template.

The EmployeeType class has not changed at all from our previous example. However, the Employee class now has one additional property as shown below:

public class Employee
{
  public Employee(string name)
  {
    Name = name;
    ManagedEmployees = new List<Employee>();
  }

  public string Name { get; set; }
  public List<Employee> ManagedEmployees { get; set; }
}

The next thing that changes in our code is the EmployeeTypes class. The constructor now needs additional code to create a list of managed employees. Below is the new code.

public class EmployeeTypes : List<EmployeeType>
{
  public EmployeeTypes()
  {
    EmployeeType type;
    Employee emp;
    Employee managed;

    type = new EmployeeType("Manager");
    emp = new Employee("Michael");
    managed = new Employee("John");
    emp.ManagedEmployees.Add(managed);
    managed = new Employee("Tim");
    emp.ManagedEmployees.Add(managed);
    type.Employees.Add(emp);

    emp = new Employee("Paul");
    managed = new Employee("Michael");
    emp.ManagedEmployees.Add(managed);
    managed = new Employee("Cindy");
    emp.ManagedEmployees.Add(managed);
    type.Employees.Add(emp);
    this.Add(type);

    type = new EmployeeType("Project Managers");
    type.Employees.Add(new Employee("Tim"));
    type.Employees.Add(new Employee("John"));
    type.Employees.Add(new Employee("David"));
    this.Add(type);
  }
}

Now that you have all of the data built in your classes, you are now ready to hook up this three-level structure to your Tree View. Figure 5 shows the complete XAML needed to hook up your three-level Tree View. You can see in the XAML that there are now two Hierarchical Data Templates and one Data Template. Again you list the Data Template first since that is the lowest level in your Tree View. The next Hierarchical Data Template listed is the next level up from the lowest level, and finally you have a Hierarchical Data Template for the first level in your tree. You need to work your way from the bottom up when creating your Tree View hierarchy. XAML is processed from the top down, so if you attempt to reference a XAML key name that is below where you are referencing it from, you will get a runtime error.

Figure 5: For three levels in a Tree View you will need two Hierarchical Data Templates and one Data Template.

Figure 5: For three levels in a Tree View you will need two Hierarchical Data Templates and one Data Template.

Each Hierarchical Data Template uses the previous template as its ItemTemplate. The ItemsSource of each Hierarchical Data Template is used to feed the data to the previous template. This is probably the most confusing part about working with the Tree View control. You are expecting the content of the current Hierarchical Data Template to use the properties set in the ItemsSource property of that template. But you need to look to the template lower down in the XAML to see the source of the data as shown in Figure 6.

Figure 6: The properties you use within the Content of a template come from the ItemsSource of the next template in the resources section.

Figure 6: The properties you use within the Content of a template come from the ItemsSource of the next template in the resources section.

Summary

Understanding how to put together your hierarchy in a Tree View is simple once you understand that you need to work from the bottom up. Start with the bottom node in your Tree View and determine what that will look like and where the data will come from. You then build the next Hierarchical Data Template to feed the data to the previous template you created. You keep doing this for each level in your Tree View until you get to the last level. The data for that last Hierarchical Data Template comes from the ItemsSource in the Tree View itself.

NOTE: You can download the sample code for this article by visiting my website at http://www.pdsa.com/downloads. Select “Tips & Tricks”, then select “WPF TreeView with Multiple Levels” from the drop down list.

 

Posted: Jul 23 2012, 10:52 AM by psheriff | with no comments
Filed under: , ,
Read XML Files using LINQ to XML and Extension Methods

In previous blog posts I have discussed how to use XML files to store data in your applications. I showed you how to read those XML files from your project and get XML from a WCF service. One of the problems with reading XML files is when elements or attributes are missing. If you try to read that missing data, then a null value is returned. This can cause a problem if you are trying to load that data into an object and a null is read. This blog post will show you how to create extension methods to detect null values and return valid values to load into your object.

The XML Data

An XML data file called Product.xml is located in the \Xml folder of the Silverlight sample project for this blog post. This XML file contains several rows of product data that will be used in each of the samples for this post. Each row has 4 attributes; namely ProductId, ProductName, IntroductionDate and Price.

<Products>
  <Product ProductId="1"
           ProductName="Haystack Code Generator for .NET"
           IntroductionDate="07/01/2010"  Price="799" />
  <Product ProductId="2"
           ProductName="ASP.Net Jumpstart Samples"
           IntroductionDate="05/24/2005"  Price="0" />
  ...
  ...
</Products>

The Product Class

Just as you create an Entity class to map each column in a table to a property in a class, you should do the same for an XML file too. In this case you will create a Product class with properties for each of the attributes in each element of product data. The following code listing shows the Product class.

public class Product : CommonBase
{
  public const string XmlFile = @"Xml/Product.xml";

  private string _ProductName;
  private int _ProductId;
  private DateTime _IntroductionDate;
  private decimal _Price;

  public string ProductName
  {
    get { return _ProductName; }
    set {
      if (_ProductName != value) {
        _ProductName = value;
        RaisePropertyChanged("ProductName");
      }
    }
  }

  public int ProductId
  {
    get { return _ProductId; }
    set {
      if (_ProductId != value) {
        _ProductId = value;
        RaisePropertyChanged("ProductId");
      }
    }
  }

  public DateTime IntroductionDate
  {
    get { return _IntroductionDate; }
    set {
      if (_IntroductionDate != value) {
        _IntroductionDate = value;
        RaisePropertyChanged("IntroductionDate");
      }
    }
  }

  public decimal Price
  {
    get { return _Price; }
    set {
      if (_Price != value) {
        _Price = value;
        RaisePropertyChanged("Price");
      }
    }
  }
}

NOTE: The CommonBase class that the Product class inherits from simply implements the INotifyPropertyChanged event in order to inform your XAML UI of any property changes. You can see this class in the sample you download for this blog post.

Reading Data

When using LINQ to XML you call the Load method of the XElement class to load the XML file. Once the XML file has been loaded, you write a LINQ query to iterate over the “Product” Descendants in the XML file. The “select” portion of the LINQ query creates a new Product object for each row in the XML file. You retrieve each attribute by passing each attribute name to the Attribute() method and retrieving the data from the “Value” property. The Value property will return a null if there is no data, or will return the string value of the attribute. The Convert class is used to convert the value retrieved into the appropriate data type required by the Product class.

private void LoadProducts()
{
  XElement xElem = null;

  try
  {
    xElem = XElement.Load(Product.XmlFile);

    // The following will NOT work if you have missing attributes
    var products =
        from elem in xElem.Descendants("Product")
        orderby elem.Attribute("ProductName").Value
        select new Product
        {
          ProductId = Convert.ToInt32(
            elem.Attribute("ProductId").Value),
          ProductName = Convert.ToString(
            elem.Attribute("ProductName").Value),
          IntroductionDate = Convert.ToDateTime(
            elem.Attribute("IntroductionDate").Value),
          Price = Convert.ToDecimal(elem.Attribute("Price").Value)
        };

    lstData.DataContext = products;
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
}

This is where the problem comes in. If you have any missing attributes in any of the rows in the XML file, or if the data in the ProductId or IntroductionDate is not of the appropriate type, then this code will fail! The reason? There is no built-in check to ensure that the correct type of data is contained in the XML file. This is where extension methods can come in real handy.

Using Extension Methods

Instead of using the Convert class to perform type conversions as you just saw, create a set of extension methods attached to the XAttribute class. These extension methods will perform null-checking and ensure that a valid value is passed back instead of an exception being thrown if there is invalid data in your XML file.

private void LoadProducts()
{
  var xElem = XElement.Load(Product.XmlFile);

  var products =
      from elem in xElem.Descendants("Product")
      orderby elem.Attribute("ProductName").Value
      select new Product
      {
        ProductId = elem.Attribute("ProductId").GetAsInteger(),
        ProductName = elem.Attribute("ProductName").GetAsString(),
        IntroductionDate =
           elem.Attribute("IntroductionDate").GetAsDateTime(),
        Price = elem.Attribute("Price").GetAsDecimal()
      };

  lstData.DataContext = products;
}

Writing Extension Methods

To create an extension method you will create a class with any name you like. In the code listing below is a class named XmlExtensionMethods. This listing just shows a couple of the available methods such as GetAsString and GetAsInteger. These methods are just like any other method you would write except when you pass in the parameter you prefix the type with the keyword “this”. This lets the compiler know that it should add this method to the class specified in the parameter.

public static class XmlExtensionMethods
{
  public static string GetAsString(this XAttribute attr)
  {
    string ret = string.Empty;

    if (attr != null && !string.IsNullOrEmpty(attr.Value))
    {
      ret = attr.Value;
    }

    return ret;
  }

  public static int GetAsInteger(this XAttribute attr)
  {
    int ret = 0;
    int value = 0;

    if (attr != null && !string.IsNullOrEmpty(attr.Value))
    {
      if(int.TryParse(attr.Value, out value))
        ret = value;
    }

    return ret;
  }

  ...
  ...
}

Each of the methods in the XmlExtensionMethods class should inspect the XAttribute to ensure it is not null and that the value in the attribute is not null. If the value is null, then a default value will be returned such as an empty string or a 0 for a numeric value.

Summary

Extension methods are a great way to simplify your code and provide protection to ensure problems do not occur when reading data. You will probably want to create more extension methods to handle XElement objects as well for when you use element-based XML. Feel free to extend these extension methods to accept a parameter which would be the default value if a null value is detected, or any other parameters you wish.

NOTE: You can download the complete sample code at my website. http://www.pdsa.com/downloads. Choose “Tips & Tricks”, then "Read XML Files using LINQ to XML and Extension Methods" from the drop-down.

Good Luck with your Coding,
Paul D. Sheriff

 

More Posts