This exercise can help you to learn most of the key essential topics for Silverlight/ WPF.
Introduction
This example can give you hands on experience on key topics of Silverlight/WPF technology. These are the must have technologies to work on XAML related platforms.
Topics to Learn
XDocument
, LinqToXML
, XmlTextWriter
for loading and saving data to XML file
ValueConverter
to convert the Modification
state to a Boolean
- XAML for displaying the data
- Two way Binding Dependency Properties of the UI controls in XAML to the data
- Delegate command pattern for command handling
- MVVM pattern
INotifyPropertyChanged
for notifying property change to UI
- Lambda Expression and Linq for data searching
Background
- Visual Studio 2010
- WPF ToolKit
Example Requirement
Objective: Create a modification tracking tool in WPF/Silverlight using the above listed technologies.
The tool should be able to perform the following:
- Button to open provided ‘
SampleModificationFile
’
- Show the
Id
column read only in grid
- Show the original text read only in grid
- Show and edit the modified text in read/write mode
- Changing the text should automatically change the state to ‘
modified
’
- Show and edit the modification state as a Boolean (is modified – read/write). This tool only cares about modified and needs-modification.
- Search capability: User should be able to search for the following fields on search button click:
Id
- int
- Original text - Un-translated -
string
- Modified text -
string
- Modified state
- Button to save the modified and searched records to XML file
UI Mockup:
Solution
I am going to demonstrate solution in WPF. Of course, you do it in Silverlight as per your convenience.
WPF Application
Create a WPF application in Visual Studio. Now first, we shall setup basic infrastructure.
Model
Create ‘model’ folder.
Add interface class ‘IModificationUnit.cs’ for entities.
[Serializable]
public class IModificationUnit
{
public string Id { get; set; }
public string Original { get; set; }
public string Modified { get; set; }
public string IsModified { get; set; }
}
Now we are going to add implementation class ‘ModificationUnit.cs’ for this interface. Collection of these properties will be bind to grid. This class is implementing INotifyPropertyChanged
so that it can notify to view for property change. IsModified
property is invoking property change to change checkbox
status when it is changed by Modified
property. Modified property change is also changing the IsModified
property.
[Serializable]
public class ModificationUnit : IModificationUnit, System.ComponentModel.INotifyPropertyChanged
{
private string _IsModified;
private string _Modified;
#region Properties
public string IsModified
{
get
{
return this._IsModified;
}
set
{
if (_IsModified != value)
{
this._IsModified = value;
if (this.PropertyChanged != null)
{
this.PropertyChanged(this,
new System.ComponentModel.PropertyChangedEventArgs("IsModified"));
}
}
}
}
public string Modified
{
get
{
return this._Modified;
}
set
{
if (_Modified != value)
{
this.IsModified = "modified";
this._Modified = value;
}
}
}
public string Id { get; set; }
public string Original { get; set; }
#endregion
#region INotifyPropertyChanged Members
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
#endregion
}
Add model class ‘ModificationToolDataModel.cs’ to get and save records. In this class, I have used LINQ and XDocument
to read and save XML. This code is self explanatory so I will be leaving it for you to understand.
class ModificationToolDataModel
{
#region "Methods for reading and saving Modification XML"
public ObservableCollection<ModificationUnit> GetModel(string filePath)
{
var modificationUnits = new ObservableCollection<ModificationUnit>();
try
{
XDocument oDoc = XDocument.Load(filePath);
var lstModificationUnits = (from info in oDoc.Descendants("modificationunit")
select new ModificationUnit
{
Id = Convert.ToString(info.Attribute("id").Value),
Original = Convert.ToString(info.Element("source").Value),
Modified = Convert.ToString(info.Element("target").Value),
IsModified = Convert.ToString(info.Element("target").Attribute
("state").Value)
}).ToList<ModificationUnit>();
lstModificationUnits.ForEach(t => { modificationUnits.Add(t); });
}
catch (Exception ex)
{
throw ex;
}
return modificationUnits;
}
public bool SaveModificationXML(List<ModificationUnit> lstModificationUnit, string filePath)
{
try
{
XmlTextWriter writer;
writer = new XmlTextWriter(filePath, null);
GenerateListToXML(lstModificationUnit).WriteTo(writer);
writer.Close();
}
catch (Exception ex)
{
throw ex;
}
return true;
}
public static XDocument GenerateListToXML(List<ModificationUnit> modificationUnitList)
{
try
{
XDocument xmlDocument = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"),
new XElement("modificationxml",
new XElement("file",
new XElement("body",
from modificationUnit in modificationUnitList
select new XElement("modificationunit",
new XAttribute("approved", "no"),
new XAttribute("id", modificationUnit.Id),
new XElement("source", modificationUnit.Original),
new XElement("target", modificationUnit.Modified,
new XAttribute("state", modificationUnit.IsModified))
)))));
return xmlDocument;
}
catch (Exception ex)
{
throw ex;
}
}
#endregion
}
ViewModel
Create viewmodel folder.
DelegateCommand.cs
To handle commands, we need to define delegate command pattern. This class can be Googled out from several places.
using System;
using System.Windows.Input;
using System.Windows;
namespace ModificationTrackingUtility.ViewModels
{
public class DelegateCommand<T> : ICommand
{
private readonly Predicate<T> canExecute;
private readonly Action<T> execute;
public event EventHandler CanExecuteChanged;
public DelegateCommand(Action<T> execute)
: this(execute, null)
{
}
public DelegateCommand(Action<T> execute,
Predicate<T> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (this.canExecute == null)
{
return true;
}
return this.canExecute((T)parameter);
}
public void Execute(object parameter)
{
execute((T)parameter);
}
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, EventArgs.Empty);
}
}
}
public class SenderParameter
{
public RoutedEventArgs EArgs { get; set; }
}
Add class file named as ModificationToolViewModel.Inherit
this file from INotifyPropertyChanged
. And create properties for controls existing in search section. We need one ObservableCollection
which can be bound to grid. I am not going in deep what is observable collection is and why it is used in WPF/Silverlight. In this class are defined 3 commands which are bound to Search, Open and Save buttons. ‘OnOpenCommand
’ opens XML file and loads XML content to grid by calling model. ‘OnApplyCommand
’ searches for the match in corresponding columns using LINQ and filters accordingly from collection. ‘OnSaveCommand
’ saves filtered and modified records to XML.
class ModificationToolViewModel : INotifyPropertyChanged
class ModificationToolViewModel : INotifyPropertyChanged
{
ModificationToolDataModel dal = new ModificationToolDataModel();
#region Properties
ObservableCollection<ModificationUnit> _ModificationUnits;
public ObservableCollection<ModificationUnit> ModificationUnits
{
get
{
return this._ModificationUnits;
}
set
{
if (_ModificationUnits != value)
{
this._ModificationUnits = value;
NotifyPropertyChanged("ModificationUnits");
}
}
}
private string _ID;
public string ID
{
get
{
return this._ID;
}
set
{
if (_ID != value)
{
this._ID = value;
NotifyPropertyChanged("ID");
}
}
}
private string _OriginalText;
public string OriginalText
{
get
{
return this._OriginalText;
}
set
{
if (_OriginalText != value)
{
this._OriginalText = value;
NotifyPropertyChanged("OriginalText");
}
}
}
private string _ModifiedText;
public string ModifiedText
{
get
{
return this._ModifiedText;
}
set
{
if (_ModifiedText != value)
{
this._ModifiedText = value;
NotifyPropertyChanged("ModifiedText");
}
}
}
private string _IsModified;
public string IsModified
{
get
{
return this._IsModified;
}
set
{
if (_IsModified != value)
{
this._IsModified = value;
NotifyPropertyChanged("IsModified");
}
}
}
public bool IsDataUnsaved { get; set; }
public DelegateCommand<SenderParameter> OpenCommand { get; private set; }
public DelegateCommand<SenderParameter> SaveCommand { get; private set; }
public DelegateCommand<SenderParameter> ApplyCommand { get; private set; }
#endregion
#region Methods
public ModificationToolViewModel()
{
_ModificationUnits = new ObservableCollection<ModificationUnit>();
this.OpenCommand = new DelegateCommand<SenderParameter>(OnOpenCommand, (c) => { return true; });
this.SaveCommand = new DelegateCommand<SenderParameter>(OnSaveCommand, (c) => { return true; });
this.ApplyCommand = new DelegateCommand<SenderParameter>
(OnApplyCommand, (c) => { return true; });
}
private void OnOpenCommand(SenderParameter e)
{
string fileName = string.Empty;
try
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Title = "ModificationXML open dialog box";
openFileDialog.InitialDirectory = @"c:\Program Files";
openFileDialog.Filter = "Modification XML files (*.xml)|*.xml";
openFileDialog.FilterIndex = 1;
openFileDialog.RestoreDirectory = true;
if (openFileDialog.ShowDialog() == true)
{
fileName = openFileDialog.FileName;
ModificationUnits = dal.GetModel(fileName);
ClearControls();
}
}
catch
{
MessageBox.Show("Some problem in opening the file.
Please check proper format of the file.");
}
}
private void OnSaveCommand(SenderParameter e)
{
try
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "Modification XML files (*.xml)|*.xml";
saveFileDialog.FilterIndex = 1;
saveFileDialog.RestoreDirectory = true;
if (saveFileDialog.ShowDialog() == true)
{
if (saveFileDialog.FileName != null)
{
dal.SaveModificationXML(ModificationUnits.ToList(), saveFileDialog.FileName);
}
else
{
MessageBox.Show("File name is not entered.");
}
MessageBox.Show("File successfully saved.");
IsDataUnsaved = false;
}
}
catch
{
MessageBox.Show("Some problem in saving the file. Please try again.");
}
}
private void OnApplyCommand(SenderParameter e)
{
try
{
var lstModificationUnit = ModificationUnits.ToList<ModificationUnit>();
if (!string.IsNullOrEmpty(ID))
{
lstModificationUnit = lstModificationUnit.Where
(modificationunit => modificationunit.Id.Equals(ID)).ToList();
}
if (!string.IsNullOrEmpty(OriginalText))
{
lstModificationUnit = lstModificationUnit.Where(modificationunit =>
modificationunit.Original.ToLower().Contains(OriginalText.ToLower())).ToList();
}
if (!string.IsNullOrEmpty(ModifiedText))
{
lstModificationUnit = lstModificationUnit.Where(modificationunit =>
modificationunit.Modified.ToLower().Contains(ModifiedText.ToLower())).ToList();
}
if (!string.IsNullOrEmpty(IsModified))
{
if (IsModified == "modified")
lstModificationUnit = lstModificationUnit.Where(modificationunit =>
modificationunit.IsModified.ToLower() != IsModified).ToList();
}
ModificationUnits.Clear();
lstModificationUnit.ForEach(t => { ModificationUnits.Add(t); });
}
catch
{
MessageBox.Show("Some problem occured in searching. Please try again.");
}
}
#region "User defined methods"
private void ClearControls()
{
ID = null;
OriginalText = null;
ModifiedText = null;
IsModified = "";
}
#endregion
#endregion
#region "INotifyPropertyChanged implementation"
protected void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
View
Have you noticed in XML file modification state is not Boolean. But check box in search section accepts only Boolean values to check/uncheck. So we need one converter to convert value back and forth. Add class file ‘BoolConverter.cs’. Implement IValueConverter
as follows:
class BoolConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (value != null)
if (value.ToString() == "modified")
return true;
return false;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if ((bool)value) return "modified"; return "needs-modification";
}
#endregion
}
Add ‘ModificationTool.xaml’ to design the interface. You can design interface by margin approach or grid base layout.
<Window x:Class="ModificationTrackingUtility.ModificationTool"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
xmlns:vm="clr-namespace:ModificationTrackingUtility.ViewModels"
xmlns:local="clr-namespace:ModificationTrackingUtility"
Title="Modification Tracking Tool" Height="505" Width="764"
Closing="Window_Closing" IsEnabled="True"
Icon="/ModificationTrackingUtility;component/color_line.ico">
<Window.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#16E9EFD6" Offset="0" />
<GradientStop Color="#FFFBFCC2" Offset="1" />
<GradientStop Color="White" Offset="0" />
</LinearGradientBrush>
</Window.Background>
<Window.DataContext>
<vm:ModificationToolViewModel />
</Window.DataContext>
<Window.Resources>
<local:BoolConverter x:Key="BoolConvert" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="695*" />
</Grid.ColumnDefinitions>
<Button Height="94" HorizontalAlignment="Left" Margin="467,5,0,0"
Name="btnOpen" VerticalAlignment="Top" Width="129"
Command="{Binding OpenCommand}"
ToolTip="Open Modification XML File Attached with project.">Open</Button>
<Button Height="94" Margin="602,5,12,0" Name="btnSave"
VerticalAlignment="Top" Command="{Binding SaveCommand}"
ToolTip="Saves to new Modification Xml file as name provided in dialog box">Save</Button>
<CheckBox Height="21" HorizontalAlignment="Left" Margin="532,404,0,0"
Name="chkModified" VerticalAlignment="Top" Width="198"
IsChecked="{Binding IsModified, Mode=TwoWay,
Converter={StaticResource BoolConvert}}">Show only un-Modified records</CheckBox>
<Label Height="23" HorizontalAlignment="Left" Margin="21,399,0,0"
Name="lblId" VerticalAlignment="Top" Width="32">Id</Label>
<Label Height="23" HorizontalAlignment="Left" Margin="151,399,0,0"
Name="lblOriginal" VerticalAlignment="Top" Width="89">Original Text</Label>
<Label Height="23" Margin="21,431,626,0" Name="lblModified"
VerticalAlignment="Top">Modified Text</Label>
<TextBox Height="21" HorizontalAlignment="Left" Margin="59,401,0,0"
Name="txtID" VerticalAlignment="Top" Width="73"
Text="{Binding ID, Mode=TwoWay}" />
<TextBox Height="21" HorizontalAlignment="Left" Margin="236,403,0,0"
Name="txtOriginal" VerticalAlignment="Top" Width="290"
Text="{Binding OriginalText, Mode=TwoWay}"/>
<TextBox Height="21" Margin="122,433,146,0" Name="txtModified"
VerticalAlignment="Top" Text="{Binding ModifiedText, Mode=TwoWay}" />
<Button Height="22" HorizontalAlignment="Right" Margin="0,431,12,0"
Name="btnApply" VerticalAlignment="Top" Width="112"
Command="{Binding ApplyCommand}">Search</Button>
<my:DataGrid ItemsSource="{Binding Path=ModificationUnits, Mode=TwoWay}"
HorizontalScrollBarVisibility="Hidden" SelectionMode="Extended"
CanUserAddRows="False" CanUserDeleteRows="False"
CanUserResizeRows="False" CanUserSortColumns="True"
AutoGenerateColumns="False" RowHeaderWidth="17"
RowHeight="25" Margin="11,105,12,105" Name="dgModificationXML"
RowEditEnding="dgModificationXML_RowEditEnding">
<my:DataGrid.Columns>
<my:DataGridTextColumn Foreground="DarkGray" Header="Id"
IsReadOnly="True" Width=".5*" Binding="{Binding Path=Id}"/>
<my:DataGridTextColumn Foreground="DarkGray" Header="Original Text"
IsReadOnly="True" Width="2*" Binding="{Binding Path=Original}"/>
<my:DataGridTextColumn Header="Modified Text" Width="2*"
Binding="{Binding Path=Modified}"/>
<my:DataGridCheckBoxColumn Header="Is Modified" Width=".8*"
Binding="{Binding Path=IsModified, UpdateSourceTrigger=PropertyChanged,
Converter={StaticResource BoolConvert}}"/>
</my:DataGrid.Columns>
</my:DataGrid>
<Label Content="Search Section" Height="28" HorizontalAlignment="Left"
Margin="12,367,0,0" Name="label1" VerticalAlignment="Top"
Width="151" FontSize="14" FontWeight="Bold" />
<Label Content="Modification Tracking Tool" Height="43"
HorizontalAlignment="Left" Margin="12,12,0,0" Name="label2"
VerticalAlignment="Top" Width="260" FontSize="18" FontWeight="Bold" />
</Grid>
</Window>
OnCloseWindow
event we need to check for unsaved data and warning message in code behind file and also setting ‘IsDataUnsaved
’ flag for edit. You can also set it on property change of IsModified
.
#region "Events"
private void dgModificationXML_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
((ModificationToolViewModel)this.DataContext).IsDataUnsaved = true;
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (((ModificationToolViewModel)this.DataContext).IsDataUnsaved)
{
MessageBoxResult result = MessageBox.Show("There is unsaved data.
Still you want to close the window?",
"Warning", MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
{
e.Cancel = true;
}
}
}
#endregion
Now, you are all done. Just run and click on open button and select given XML file. It will fill the grid with XML data. Modify ‘Modified text’, you can see it automatically checks ‘IsModified
’ checkbox if not checked. You can also filter records on search button click.
Summary
In this article, you have learned most of the essential technologies for WPF/Silverlight. Hope this would be pretty simple example to elaborate scenarios.
If this article helps you in preparing for WPF/Silverlight, don’t forget to hit voting option. You can also read my other articles.
Happy coding!!