Click here to Skip to main content
15,887,214 members
Articles / Desktop Programming / WPF

Hands on Lab for Key Features Used in Silverlight/WPF Projects

Rate me:
Please Sign up or sign in to vote.
3.67/5 (3 votes)
5 Jun 2012CPOL4 min read 13.3K   282   7   4
Hands on Lab for MVVM, Value Converter, XAML Binding, Delegate Command pattern, INotifyPropertyChange, Lambda Expression, LINQ etc. in Silverlight/WPF project

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:

Image 1

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.

C#
[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.

C#
//Following properties are user in collection which is bound to grid
/// <summary>
/// Modification Unit
/// </summary>
[Serializable]
public class ModificationUnit : IModificationUnit, System.ComponentModel.INotifyPropertyChanged
{
    private string _IsModified;
    private string _Modified;
    #region Properties
    /// <summary>
    /// Is Modified
    /// </summary>
    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"));
                }
            }
        }
    }
    /// <summary>
    /// Modified Text
    /// </summary>
    public string Modified
    {
        get
        {
            return this._Modified;
        }
        set
        {
            if (_Modified != value)
            {
                this.IsModified = "modified";
                this._Modified = value;
            }
        }
    }
    //Auto Properties
    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.

C#
class ModificationToolDataModel
{
    #region "Methods for reading and saving Modification XML"
    /// <summary>
    /// Open Modification XML file
    /// </summary>
    /// <param name="filePath">File path</param>
    /// <returns>dto</returns>
    public ObservableCollection<ModificationUnit> GetModel(string filePath)
    {
        var modificationUnits = new ObservableCollection<ModificationUnit>();
        try
        {
            XDocument oDoc = XDocument.Load(filePath);
            //reading file through linq
            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;
    }
    /// <summary>
    /// Save Modification XML file
    /// </summary>
    /// <param name="lstModificationUnit"> list of ModificationUnit</param>
    /// <param name="filePath">destination path</param>
    /// <returns>Flag for successful save</returns>
    public bool SaveModificationXML(List<ModificationUnit> lstModificationUnit, string filePath)
    {
        try
        {
            //saving file
            XmlTextWriter writer;
            writer = new XmlTextWriter(filePath, null);
            GenerateListToXML(lstModificationUnit).WriteTo(writer);
            writer.Close();
        }
        catch (Exception ex)
        {
            throw ex;
        }
        return true;
    }
    /// <summary>
    /// This Method will generate list TO XML using LINQ
    /// </summary>
    /// <param name="modificationUnitList">List of ModificationUnits</param>
    /// <returns>XDocument</returns>
    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.

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

C#
class ModificationToolViewModel : INotifyPropertyChanged
{
    ModificationToolDataModel dal = new ModificationToolDataModel();
    #region Properties
    ObservableCollection<ModificationUnit> _ModificationUnits;       
    /// <summary>
    /// Collection bound to grid
    /// </summary>
    public ObservableCollection<ModificationUnit> ModificationUnits
    {
        get
        {
            return this._ModificationUnits;
        }
        set
        {
            if (_ModificationUnits != value)
            {
                this._ModificationUnits = value;
                NotifyPropertyChanged("ModificationUnits");
            }
        }
    }
    //Following properties are used to bind to controls of Search section
    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; }
    //Commands
    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; });
    }
    /// <summary>
    /// Opens xml file and loads xml content to grid by calling model
    /// </summary>
    /// <param name="e"></param>
    private void OnOpenCommand(SenderParameter e)
    {
        string fileName = string.Empty;
        try
        {
            //Open File Dialog
            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;
                //Call dal to load file
                ModificationUnits = dal.GetModel(fileName);
                //Clear filter controls
                ClearControls();
            }
        }
        catch
        {
            MessageBox.Show("Some problem in opening the file. 
                             Please check proper format of the file.");
        }
    }
    /// <summary>
    /// Saves filtered and modified records to xml
    /// </summary>
    /// <param name="e"></param>
    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)
                {
                    //calling dal to save
                    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.");
        }
    }
    /// <summary>
    /// Filters record from grid
    /// </summary>
    /// <param name="e"></param>
    private void OnApplyCommand(SenderParameter e)
    {
        try
        {
            var lstModificationUnit = ModificationUnits.ToList<ModificationUnit>();
            //Searching using lambda expression and INotifyPropertyChanged
            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)) //CheckBox
            {
                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"
    /// <summary>
    /// Clears all filter controls
    /// </summary>
    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:

C#
/// <summary>
/// Class for converting value
/// </summary>
class BoolConverter : IValueConverter
{
    #region IValueConverter Members
    /// <summary>
    /// Convert
    /// </summary>
    /// <param name="value"></param>
    /// <param name="targetType"></param>
    /// <param name="parameter"></param>
    /// <param name="culture"></param>
    /// <returns></returns>
    public object Convert(object value, Type targetType, object parameter, 
                          System.Globalization.CultureInfo culture)
    {
        if (value != null)
            if (value.ToString() == "modified")
                return true;
        return false;
    }
    /// <summary>
    /// ConvertBack
    /// </summary>
    /// <param name="value"></param>
    /// <param name="targetType"></param>
    /// <param name="parameter"></param>
    /// <param name="culture"></param>
    /// <returns></returns>
    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.

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

C#
#region "Events"
/// <summary>
/// To set grid editing state
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void dgModificationXML_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
    ((ModificationToolViewModel)this.DataContext).IsDataUnsaved = true;
}
/// <summary>
/// Prompt to save on close
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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 .

Happy coding!!

License

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


Written By
Architect
United States United States
Manoj Kumar

AWS Certified Solutions Architect (Web development, Serverless, DevOps, Data)
MSSE - Cloud and Mobile Computing, San Jose State University

A wide range of experience in resolving complex business problems.

* Cloud Technologies: EC2, S3, DynamoDB & RDS databases, Lambda serverless architecture, Microservices architecture, API Gateway, Cloud Front CDN, Linux/Windows systems administration, CloudFormation, DevOps, Docker, CICD, Node.js, Python, Java and other open source technologies. Familiarity with OpenStack.
* Web Technologies: HTML5, Node.Js, MEAN Stack, AngularJS, ASP.Net Core, MVC5, CSS3, jQuery, Bootstrap, MongoDB, JavaScript, JSON, AJAX.
* Data: Experience in database architecture, Big Data, Machine Learning, BI, Data Analytics, No-SQL databases, ETL.
* Mobile: IOS/Android app development

He lives with his wife Supriya and daughter Tisya in Bay Area.

Comments and Discussions

 
QuestionHow can I return the whole dataset on Search Pin
sdnd200020-Sep-12 9:35
sdnd200020-Sep-12 9:35 
AnswerRe: How can I return the whole dataset on Search Pin
ManojKumar1924-Sep-12 7:15
ManojKumar1924-Sep-12 7:15 
Question[My vote of 1] 1 - Not An Article Pin
Kevin Marois5-Jun-12 8:04
professionalKevin Marois5-Jun-12 8:04 
AnswerRe: [My vote of 1] 1 - Not An Article Pin
ManojKumar195-Jun-12 9:12
ManojKumar195-Jun-12 9:12 

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

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