Creating Master-Details Application using WPF 3.5 and LINQ to XML
In this article we will see how WPF 3.5 makes use of LINQ to XML. By now, I assume most of you are well aware of new features on .NET 3.5. Language Integrated Queries (LINQ) is one of the most important features of .NET 3.5. LINQ provides flavours like LINQ to Objects, LINQ to SQL and LINQ to XML. There are lots of advantages of using LINQ to XML, this reduce lots of complexities of XML DOM.
In WPF 3.0, we had been provided the XmlProvider, using which it was possible to bind a XML file with WPF elements. However in 3.0 it was necessary for us to use XPATH expressions for filtering data from XML and show the data in WPF elements. With WPF 3.5, it is now very easy to read a XML file using LINQ to Xml classes and filter the data. Let us develop a WPF 3.5 application to demonstrate this. In this application we will use following:
· XML file as Data Source.
· LINQ to Xml for Data Filtering.
Step 1: Open VS2008 and create a WPF Windows Application, name this as 'WPF_UsingLinqDataSource'.
Step 2: In this project add a XML file 'Company.xml' as below:
<?xml version="1.0" encoding="utf-8" ?>
<Company Xmlns="">
<Department name="Training">
<COE name=".NET">
<Employee name="Mahesh">
<no>1</no>
</Employee>
<Employee name="Ravi">
<no>2</no>
</Employee>
<Employee name="Subodh">
<no>3</no>
</Employee>
<Employee name="Pravin">
<no>4</no>
</Employee>
<Employee name="Surekha">
<no>5</no>
</Employee>
</COE>
<COE name="Object">
<Employee name="Umar">
<no>7</no>
</Employee>
<Employee name="Abhijit">
<no>8</no>
</Employee>
<Employee name="Sudhir">
<no>8</no>
</Employee>
<Employee name="Ajay">
<no>9</no>
</Employee>
<Employee name="Manoj">
<no>10</no>
</Employee>
<Employee name="Rakesh">
<no>11</no>
</Employee>
</COE>
<COE name="Java">
<Employee name="Kapil">
<no>12</no>
</Employee>
<Employee name="Chetan">
<no>13</no>
</Employee>
<Employee name="Amar">
<no>14</no>
</Employee>
<Employee name="Nita">
<no>15</no>
</Employee>
<Employee name="Arti">
<no>16</no>
</Employee>
<Employee name="Jayesh">
<no>17</no>
</Employee>
</COE>
</Department>
<Department name ="Testing">
<COE name="Automated">
<Employee name="Rajesh">
<no>18</no>
</Employee>
<Employee name="Kumar">
<no>19</no>
</Employee>
<Employee name="Ashutosh">
<no>20</no>
</Employee>
<Employee name="Mohan">
<no>21</no>
</Employee>
<Employee name="Nandini">
<no>22</no>
</Employee>
<Employee name="Shailaja">
<no>23</no>
</Employee>
</COE>
<COE name="Mannual">
<Employee name="Aparna">
<no>24</no>
</Employee>
<Employee name="Madhuri">
<no>25</no>
</Employee>
<Employee name="Sonal">
<no>26</no>
</Employee>
<Employee name="Rajnish">
<no>27</no>
</Employee>
<Employee name="Suman">
<no>28</no>
</Employee>
<Employee name="Tarun">
<no>29</no>
</Employee>
</COE>
</Department>
</Company>
Step 3: In the project add a new class, 'CXmlFileLoader.cs' as below:
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
namespace WPF_UsingLinqDataSource
{
public class CXmlFileLoader
{
public XElement LoadXml(string xFilePath)
{
XElement xEle = XElement.Load(xFilePath);
return xEle;
}
}
}
VB.NET
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.Xml.Linq
Namespace WPF_UsingLinqDataSource
Public Class CXmlFileLoader
Public Function LoadXml(ByVal xFilePath As String) As XElement
Dim xEle As XElement = XElement.Load(xFilePath)
Return xEle
End Function
End Class
End Namespace
The above class is used to load an XML file. This class is used in the WPF application using 'ObjectDataProvider'.
Step 4: In the project add a new class 'CEmployee.cs'. This class is used for storing Employee details when they are filtered from the XML file.
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace WPF_UsingLinqDataSource
{
public class CEmployee
{
public string EmpName { get; set; }
public string EmpNo { get; set; }
}
}
VB.NET
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Namespace WPF_UsingLinqDataSource
Public Class CEmployee
Private privateEmpName As String
Public Property EmpName() As String
Get
Return privateEmpName
End Get
Set(ByVal value As String)
privateEmpName = value
End Set
End Property
Private privateEmpNo As String
Public Property EmpNo() As String
Get
Return privateEmpNo
End Get
Set(ByVal value As String)
privateEmpNo = value
End Set
End Property
End Class
End Namespace
Step 5: Now create a XAML file as below: (Note: Please use Drag-Drop).
<Window x:Class="WPF_UsingLinqDataSource.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="632" Width="1113"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:LINQ="clr-namespace:System.Xml.Linq;assembly=System.Xml.Linq"
xmlns:src="clr-namespace:WPF_UsingLinqDataSource">
<Window.Resources>
<!--The Class for Loading the Xml file using LINQ XML API-->
<ObjectDataProvider x:Key="EmpDs" ObjectType="{x:Type src:CXmlFileLoader}" MethodName="LoadXml">
<ObjectDataProvider.MethodParameters>
<sys:String>G:\WPF_Techforge\WPF_UsingLinqDataSource\WPF_UsingLinqDataSource\
Company.xml</sys:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<!--Define DataTemplate for Data Filtering-->
<DataTemplate x:Key="DtDname">
<TextBlock Text="{Binding Path=Attribute[name].Value}"></TextBlock>
</DataTemplate>
</Window.Resources>
<Grid Height="616" DataContext="{Binding Source={StaticResource EmpDs}}">
<Grid.RowDefinitions>
<RowDefinition Height="316"></RowDefinition>
<RowDefinition Height="316"></RowDefinition>
</Grid.RowDefinitions>
<Grid Margin="0,0,0,101" Grid.RowSpan="2">
<ComboBox Height="23" HorizontalAlignment="Left"
Margin="12,54,0,0" Name="lstDept"
VerticalAlignment="Top"
Width="181"
ItemsSource="{Binding Path=Elements[Department]}"
ItemTemplate="{StaticResource DtDname}" IsSynchronizedWithCurrentItem="True">
</ComboBox>
<Label Height="28" HorizontalAlignment="Left" Margin="12,20,0,0" Name="label1" VerticalAlignment="Top" Width="181">Department</Label>
<ComboBox Height="23" HorizontalAlignment="Left"
Margin="215,54,0,0" Name="lstCOE"
VerticalAlignment="Top" Width="181"
DataContext="{Binding ElementName=lstDept,Path=SelectedItem}"
ItemsSource="{Binding Path=Descendants[COE]}"
ItemTemplate="{StaticResource DtDname}"
IsSynchronizedWithCurrentItem="True">
</ComboBox>
<Label Height="28" HorizontalAlignment="Left" Margin="215,12,0,0" Name="label2" VerticalAlignment="Top" Width="181">COE Name</Label>
<Label Height="28" Margin="416,20,474,0" Name="label3" VerticalAlignment="Top">Employee Name</Label>
<ComboBox Height="23" Margin="416,54,474,0"
Name="lstEmp"
VerticalAlignment="Top"
DataContext="{Binding ElementName=lstCOE,Path=SelectedItem}"
ItemsSource="{Binding Path=Descendants[Employee]}"
ItemTemplate="{StaticResource DtDname}" IsSynchronizedWithCurrentItem="True"
/>
<Grid HorizontalAlignment="Right" Margin="0,26.143,14,244.857" Name="grid1" Width="419"
DataContext="{Binding ElementName=lstEmp,Path=SelectedItem}">
<Grid.RowDefinitions>
<RowDefinition Height="62*" />
<RowDefinition Height="47.536*" />
<RowDefinition Height="171.464*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="212*" />
<ColumnDefinition Width="207*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="1" Margin="6,9,86,0" Name="label6" Height="65" Grid.RowSpan="2" VerticalAlignment="Top">Emp Name</Label>
<TextBox Grid.Column="1" Grid.Row="1" Margin="0,9,26,4.536" Name="txtename" Text="{Binding Path=Attribute[name].Value}"/>
<Label Margin="0,3,92,6.509" Name="label7">EmpNo</Label>
<TextBox Grid.Column="1" Margin="0,8,34,23" Name="txteno" Text="{Binding Path=Element[no].Value}"/>
</Grid>
<Grid DataContext="{Binding Source={StaticResource EmpDs}}" Margin="215,83,0,237" HorizontalAlignment="Left" Width="205">
<my:DataGrid AutoGenerateColumns="False" Margin="20,19,6,18"
Name="dgEmp" xmlns:my="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
ItemsSource="{Binding}">
<my:DataGrid.Columns>
<my:DataGridTextColumn Header="Employee Name" Binding="{Binding Path=EmpName}"></my:DataGridTextColumn>
<my:DataGridTextColumn Header="Employee No" Binding="{Binding Path=EmpNo}"></my:DataGridTextColumn>
</my:DataGrid.Columns>
</my:DataGrid>
</Grid>
</Grid>
</Grid>
</Window>
In the Xaml shown above, the 3 assemblies referred are explained as below:
· sys: is used to include 'mscorlib' for referring System namespace.
· LINQ: is used to include 'System.Xml.Linq' for referring linq to xml classes.
· src: is used for referring classes in the current project.
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:LINQ="clr-namespace:System.Xml.Linq;assembly=System.Xml.Linq"
xmlns:src="clr-namespace:WPF_UsingLinqDataSource"
ObjectDataProvider, eclared into the windows resource dictionary, is used to refer the 'CXmlFileLoader' class. The objectDataProvider makes an object of the class and calls 'LoadXml' method from the class. This takes a string parameter that is the name of xml file.
<ObjectDataProvider x:Key="EmpDs" ObjectType="{x:Type src:CXmlFileLoader}" MethodName="LoadXml">
<ObjectDataProvider.MethodParameters>
<sys:String>G:\WPF_Techforge\WPF_UsingLinqDataSource\WPF_UsingLinqDataSource
\Company.xml</sys:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
DataTemplate, is also declared in the windows resource dictionary. This represents the structure of the data to be displayed. This contains the TextBlock element which is used to bind to the 'name' attribute of the xml element from the file. DataTemplate generally is used when the collection data needs to be displayed into Items family controls of WPF like ListBox, Combobox, ListView etc. DataTemplate is managed by the 'FrameworkElementFactory' class. This class is used to construct VisualTree based upon the DataTemplate structure.
<!--Define DataTemplate for Data Filtering-->
<DataTemplate x:Key="DtDname">
<TextBlock Text="{Binding Path=Attribute[name].Value}"></TextBlock>
</DataTemplate>
The key of the ObjectDataProvider is assigned to the datacontext property of the Grid, element. This is the parent element of all controls on the window.
DataContext="{Binding Source={StaticResource EmpDs}}"
In the Xaml there are 3 combo boxes named lstDept, lstCOE and lstEmp. These three are related with each other using master-details relationship. So for each combo box, properties are declared as below:
Combo box 'lstDept':
<ComboBox Height="23" HorizontalAlignment="Left"
Margin="12,54,0,0" Name="lstDept"
VerticalAlignment="Top"
Width="181"
ItemsSource="{Binding Path=Elements[Department]}"
ItemTemplate="{StaticResourceDtDname}" IsSynchronizedWithCurrentItem="True">
ItemsSource is assigned by the collection of all Departments from the Xml file. Since the parent Grid’s DataContext property is already assigned to the objectDataProvider, the expression 'Elements[Department]' automatically reads all the 'Department' XElement children. From this collection, using 'ItemTemplate=”{StaticResource DtDname}”' expression reads all those Department elements having 'name' attribute. IsSynchronizedWithCurrentItem, property synchronize data from the 'lstDept' with the other UI Elements on the Window.
Combo box 'lstCOE':
<ComboBox Height="23" HorizontalAlignment="Left"
Margin="12,54,0,0" Name="lstDept"
VerticalAlignment="Top"
Width="181"
ItemsSource="{Binding Path=Elements[Department]}"
ItemTemplate="{StaticResource DtDname}" IsSynchronizedWithCurrentItem="True">
DataContext property for the 'lstCOE' is set based upon the 'SelectedItem' result from the 'lstDept'. This will return a xml subtree for the selected department from 'lstDept'. ItemsSource is now all descendants of the 'Department' element that is the collection of all 'COE' elements under that department. ItemTemplate will display all those COEs having 'name' attribute. IsSynchronizedWithCurrentItem will synchronize data with other UI elements on the window.
Combo box 'lstEmp':
<ComboBox Height="23" Margin="416,54,474,0"
Name="lstEmp"
VerticalAlignment="Top"
DataContext="{Binding ElementName=lstCOE,Path=SelectedItem}"
ItemsSource="{Binding Path=Descendants[Employee]}"
ItemTemplate="{StaticResource DtDname}" IsSynchronizedWithCurrentItem="True"
/>
The above combobox will populate data based upon COE selection form 'lstCOE'. This is a collection of all employees under selected COE.
The 'lstEmp' now synchronizes data with Textboxes as below:
<Grid HorizontalAlignment="Right" Margin="0,26.143,14,244.857" Name="grid1" Width="419"
DataContext="{Binding ElementName=lstEmp,Path=SelectedItem}">
<Grid.RowDefinitions>
<RowDefinition Height="62*" />
<RowDefinition Height="47.536*" />
<RowDefinition Height="171.464*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="212*" />
<ColumnDefinition Width="207*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="1" Margin="6,9,86,0" Name="label6" Height="65" Grid.RowSpan="2" VerticalAlignment="Top">Emp Name</Label>
<TextBox Grid.Column="1" Grid.Row="1" Margin="0,9,26,4.536" Name="txtename" Text="{Binding Path=Attribute[name].Value}"/>
<Label Margin="0,3,92,6.509" Name="label7">EmpNo</Label>
<TextBox Grid.Column="1" Margin="0,8,34,23" Name="txteno" Text="{Binding Path=Element[no].Value}"/>
</Grid>
These two textboxes populates values based on the selection from 'lstEmp'. The 'SelectedItem' returns Employee subtree from which 'Attribute[name]' is displayed into txtename and 'Element[no]' is displayed into txteno.
The Xaml also contains DataGrid which is configured as below:
<Grid DataContext="{Binding Source={StaticResource EmpDs}}" Margin="215,83,0,237" HorizontalAlignment="Left" Width="205">
<my:DataGrid AutoGenerateColumns="False" Margin="20,19,6,18"
Name="dgEmp" xmlns:my="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
ItemsSource="{Binding}">
<my:DataGrid.Columns>
<my:DataGridTextColumn Header="Employee Name" Binding="{Binding Path=EmpName}"></my:DataGridTextColumn>
<my:DataGridTextColumn Header="Employee No" Binding="{Binding Path=EmpNo}"></my:DataGridTextColumn>
</my:DataGrid.Columns>
</my:DataGrid>
</Grid>
DataGrid is put inside a Grid, which is bound to the objectDataProvider using DataContext property. ItemsSource property of the DataGrid is set to the same source as that of the Grid. DataGrid contains two textbox columns. These are bound with 'EmpName' and 'EmpNo' properties of 'CEmployee' class.
Now if you see the design view of WPF, following design will displayed:
Since at design all binding is done, you will see default selection in combo boxes and data synchronization with textboxes.
Step 6: Open Window1.xaml.cs and at class level declare the following variable:
C#
public static XElement empList;
VB.NET
Public Shared empList As XElement
Step 7: In the Window1 constructor write the following code: (Note: InitializeComponent is already available).
C#
public Window1()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Window1_Loaded);
this.lstCOE.SelectionChanged += new SelectionChangedEventHandler(lstCOE_SelectionChanged);
}
VB.NET
Public Sub New()
InitializeComponent()
AddHandler Loaded, AddressOf Window1_Loaded
AddHandler lstCOE.SelectionChanged, AddressOf lstCOE_SelectionChanged
End Sub
Step 8: On Loaded event of the window write the following code:
C#
void Window1_Loaded(object sender, RoutedEventArgs e)
{
empList = (this.FindResource("EmpDs") as ObjectDataProvider).Data as XElement;
}
VB.NET
Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
empList = TryCast((TryCast(Me.FindResource("EmpDs"), ObjectDataProvider)).Data, XElement)
End Sub
The above code locates 'EmpDs' from the resource dictionary and casts it to the XElement. The object 'empList' now caches the complete Xml tree from the XML document.
Step 9: Write the following code in 'lstCOE_SelectionChanged' method:
C#
void lstCOE_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
XElement listSelectedData = lstCOE.SelectedValue as XElement;
string listAttributedData = listSelectedData.Attribute("name").Value;
//The Collection object for Storing the Data
ObservableCollection<CEmployee> empData = new ObservableCollection<CEmployee>();
var nodeDepartment = from Emp in empList.Descendants("Department").Elements("COE")
where Emp.Attribute("name").Value == listAttributedData
select Emp;
foreach (var item in nodeDepartment)
{
var coeResult = from emp in nodeDepartment.Descendants("Employee")
select new CEmployee
{
EmpName = emp.Attribute("name").Value, //Reads the Attribute EmpName
EmpNo = emp.Element("no").Value //Reads the EMployee No
};
//Now Put the data in collection
foreach (var res in coeResult)
{
empData.Add(res);
}
}
//Assign the Data Source
dgEmp.DataContext = empData;
}
VB.NET
Private Sub lstCOE_SelectionChanged(ByVal sender As Object, ByVal e As SelectionChangedEventArgs)
Dim listSelectedData As XElement = TryCast(lstCOE.SelectedValue, XElement)
Dim listAttributedData As String = listSelectedData.Attribute("name").Value
'The Collection object for Storing the Data
Dim empData As New ObservableCollection(Of CEmployee)()
Dim nodeDepartment = From Emp In empList.Descendants("Department").Elements("COE") _
Where Emp.Attribute("name").Value = listAttributedData _
Select Emp
For Each item In nodeDepartment
Dim coeResult = From emp In nodeDepartment.Descendants("Employee") _
Select New CEmployee
emp.Attribute("name").Value, EmpNo = emp.Element("no").Value
EmpName = emp.Attribute("name").Value, EmpNo
'Now Put the data in collection
For Each res In coeResult
empData.Add(res)
Next res
Next item
'Assign the Data Source
dgEmp.DataContext = empData
End Sub
The above code is used to display all Employees for a selected COE from 'lstCOE' using following code:
C#
var nodeDepartment = from Emp in empList.Descendants("Department").Elements("COE")
where Emp.Attribute("name").Value == listAttributedData select Emp;
VB.NET
Dim nodeDepartment = From Emp In empList.Descendants("Department").Elements("COE") _
Where Emp.Attribute("name").Value = listAttributedData _
Select Emp
The following code puts employees from the selected COE into the CEmployee class.
C#
foreach (var item in nodeDepartment)
{
var coeResult = from emp in nodeDepartment.Descendants("Employee")
select new CEmployee
{
EmpName = emp.Attribute("name").Value, //Reads the Attribute EmpName
EmpNo = emp.Element("no").Value //Reads the EMployee No
};
//Now Put the data in collection
foreach (var res in coeResult)
{
empData.Add(res);
}
}
VB.NET
For Each item In nodeDepartment
Dim coeResult = From emp In nodeDepartment.Descendants("Employee") _Select New CEmployee
emp.Attribute("name").Value, EmpNo = emp.Element("no").Value EmpName = emp.Attribute("name").Value, EmpNo
'Now Put the data in collection
For Each res In coeResult
empData.Add(res)
Next res
Next item
Note: The code may have gotten a bit complex here, but I found it appropriate to my application. You are free to fine tune it the way you prefer.
Step 10: Run the application. When you select a specific COE, employees from that COE will be displayed in DataGrid demonstrating a Master-Detail application using WPF 3.5 and LINQ to XML:
Conclusion: LINQ to XML has provided a better programmer oriented mechanism for working with XML documents. This reduces lots of complexities while working with typical XML DOM, where we earlier needed to use complex XPATH expressions. Linq To XML provides easy to use and understandable classes and methods that can be used on XML documents. One of the best thing this article explains is that classes from System.Xml.Linq can directly be used in XAML for Data binding in WPF applications.
The entire source code of this article can be downloaded over here
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Mahesh Sabnis is a DotNetCurry author and a Microsoft MVP having over two decades of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions), and Front-end technologies like Angular and React. Follow him on twitter @
maheshdotnet or connect with him on
LinkedIn