Custom Data Binding, Data Navigation and Data Validation in WPF

Getting Started:

Create a new WPF Project.

  1. Open Visual Studio 2010.
  2. Go to File => New => Project
  3. Select Windows in installed templates
  4. Select WPF Application
  5. Enter the Name and choose the location.
  6. Click OK


First of all add a new class to the project.

Right click on your project name and select the Add New Item from the menu and rename your class.

I create a class called MyFriends that is listed in Listing 1. I also inherit this class from INotifyPropertyChanged. This we need to capture notifications when a property value is changed for this class. As you can see from the below code, the MyFriends class has fields name, age, country, profession, mobileno and favQuote. These fields are exposed as public properties.

MyFriends.cs

using System;
using System.Windows;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes; 

class MyFriends : INotifyPropertyChanged
{
        // INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        protected void Notify(string propName)
        {
            if (this.PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
            }
        }
        string name;
        public string Name
        {
            get
            {
                return this.name;
            }
            set
            {
                if (this.name == value)
                {
                    return;
                }
                this.name = value;
                Notify("Name");
            }
        }
       int age;
       public int Age
       {
              get
              {
                  return this.age;
              }
              set
              {
                if(this.age == value )
                  {
                     return;
                  }
                  this.age = value;
                  Notify("Age");
              }
       }
       string country;
       public string Country
       {
           get
           {
               return this.country;
           }
           set
           {
               if (this.country == value)
               {
                   return;
               }
               this.country = value;
               Notify("Country");
           }
       }
       string profession;
       public string Profession
       {
           get
           {
               return this.profession;
           }
           set
           {
               if (this.profession == value)
               {
                   return;
               }
               this.profession = value;
               Notify("Profession");
           }
       }
       string mobileNo;
       public string MobileNo
       {
           get
           {
               return this.mobileNo;
           }
           set
           {
               if (this.mobileNo == value)
               {
                   return;
               }
               this.mobileNo = value;
               Notify("MobileNo");
           }
       }
       string favQuote;
       public string FavQuote
       {
           get
           {
               return this.favQuote;
           }
           set
           {
               if (this.favQuote == value)
               {
                   return;
               }
               this.favQuote = value;
               Notify("FavQuote");
           }
       }
       public MyFriends() { }
       public MyFriends(string name, int age, string country, string profession, string mobileNo, string favQuote)
       {
            this.name = name;
            this.age = age;
            this.country = country;
            this.profession = profession;
            this.mobileNo = mobileNo;
            this.favQuote = favQuote;
        }
}

Listing 1


Now, I add a class called Friends as listed in Listing 2. This class is a collection of MyFriends.

 class Friends : List<MyFriends>
    {
    }

Listing 2

I add three more classes called AgeToForegroundConverter, Base16Converter and NumberRangeValue as listed in Listing 3. These classes are used to convert various data types. 

[ValueConversion(/*sourceType*/ typeof(int), /*targetType*/ typeof(Brush))]
    public class AgeToForegroundConverter : IValueConverter
    {
        // Called when converting the Age to a Foreground brush
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            // Only convert to brushes...
            if (targetType != typeof(Brush))
            {
                return null;
            }
            // DANGER! After 25, it's all downhill...
            int age = int.Parse(value.ToString());
            return (age > 25 ? Brushes.Red : Brushes.Black);
        }
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            // Should not be called in our example
            throw new NotImplementedException();
        }
    }  

public class Base16Converter : IValueConverter
{
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            // Convert to base 16
            return ((int)value).ToString("x");
        }
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            // Convert from base 16
            return int.Parse((string)value, System.Globalization.NumberStyles.HexNumber);
        }
}
public class NumberRangeRule : ValidationRule
{
        int min;
        public int Min
        {
            get { return min; }
            set { min = value; }
        }
        int max;
        public int Max
        {
            get { return max; }
            set { max = value; }
        }
        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        {
            int number;
            if (!int.TryParse((string)value, out number))
            {
                return new ValidationResult(
                false,
                "Invalid number format");
            }
            if (number < min || number > max)
            {
                return new ValidationResult(
                false,
                string.Format("Number out of range ({0}-{1})", min, max));
            }
            //return new ValidationResult(true, null); // valid result
            return ValidationResult.ValidResult; // static valid result
            // to save on garbage
        }
}

Listing 3


Now, let's focus on building the UI.

My Window.xaml file looks like Listing 4. First, I create some resources and then add several controls to the Window.


Window.xaml.

<Window x:Class="TestWpf.BindListData"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:class="clr-namespace:TestWpf"
        Title="BindListData" Height="400" Width="400">
<Window.Resources>
        <class:Friends x:Key="MyGoodFriends">
            <class:MyFriends Name="Raj Beniwal" Age="28" Country="India" Profession="Software Development" MobileNo="9899999990" FavQuote="Nothing is impossible"></class:MyFriends>
            <class:MyFriends Name="Mahesh Chand" Age="38" Country="USA" Profession="Technical Consultant" MobileNo="9899999990" FavQuote="Not posted"></class:MyFriends>
                <class:MyFriends Name="Sameer Mathotra" Age="30" Country="Dubai" Profession="Software Engineer" MobileNo="9899999990" FavQuote="Not posted"></class:MyFriends>
                    <class:MyFriends Name="Vikash Nanda" Age="25" Country="Australia" Profession="Mechanical Engineer" MobileNo="9899999990" FavQuote="Not posted"></class:MyFriends>
                        <class:MyFriends Name="Sidharth Thakur" Age="27" Country="New Zealand" Profession="Project Manager" MobileNo="9899999990" FavQuote="Not posted"></class:MyFriends>
                            <class:MyFriends Name="Shivam Tripathy" Age="32" Country="London" Profession="HR Consultant" MobileNo="9899999990" FavQuote="Not posted"></class:MyFriends>
                                <class:MyFriends Name="Rohit Khanna" Age="35" Country="India" Profession="Businessman" MobileNo="9899999990" FavQuote="Not posted"></class:MyFriends>
        </class:Friends>      

<!--<class:AgeToForegroundConverter x:Key="ageConverter" />-->       
<class:Base16Converter x:Key="ageConverter"></class:Base16Converter>  
</Window.Resources>
    <Grid DataContext="{StaticResource MyGoodFriends}" Width="386">
        <TextBlock Margin="12,24,331,320">Name:</TextBlock>
        <TextBox Text="{Binding Path=Name}" Margin="86,24,12,307" IsReadOnly="True" />
        <TextBox  Foreground="{Binding Path=Age, Converter={StaticResource ageConverter}}" Margin="86,69,11,265" x:Name="txtAge">
            <TextBox.Text>
                <Binding Path="Age" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
                    <Binding.ValidationRules>
                        <class:NumberRangeRule Min="0" Max="100"></class:NumberRangeRule>
                    </Binding.ValidationRules>
                </Binding>               
            </TextBox.Text>
        </TextBox>
        <Button Margin="86,319,54,12" x:Name="btnBirthday" Click="btnBirthday_Click">When is my Birthday</Button>
        <TextBlock Margin="12,69,0,274" HorizontalAlignment="Left" Width="34">Age:</TextBlock>
        <Button Content="Back" Margin="272,292,0,48" Click="btnBack_Click" x:Name="btnBack" HorizontalAlignment="Left" Width="47" />
        <Button Content="Next" Margin="0,292,13,48" x:Name="btnForward" Click="btnForward_Click" HorizontalAlignment="Right" Width="48" />
        <TextBlock HorizontalAlignment="Left" Margin="12,113,0,226" Width="54">Country:</TextBlock>
        <TextBlock HorizontalAlignment="Left" Margin="13,0,0,189" Width="63" Height="22" VerticalAlignment="Bottom">Profession:</TextBlock>
        <TextBox IsReadOnly="True" Margin="87,105,11,226" Text="{Binding Path=Country}" />
        <TextBox IsReadOnly="True" Margin="87,147,11,189" Text="{Binding Path=Profession}" />
        <TextBlock Height="22" HorizontalAlignment="Left" Margin="13,0,0,159" VerticalAlignment="Bottom" Width="63">Mobile No.:</TextBlock>
        <TextBox IsReadOnly="True" Margin="87,182,12,154" Text="{Binding Path=MobileNo}" />
        <TextBlock Height="22" HorizontalAlignment="Left" Margin="12,0,0,124" VerticalAlignment="Bottom" Width="63">Fav Quote:</TextBlock>
        <TextBox IsReadOnly="True" Margin="86,215,13,82" Text="{Binding Path=FavQuote}" />
    </Grid>
</Window> 

Listing 4


Now, let's create and get data and bind it to the UI. The code listed in Listing 5 has a method called GetFriendsView. This method gets data from the XAML using the FindResources method and returns a collection. The Final UI looks like Figure 1.

.xaml.cs 

public partial class BindListData : Window
{
        public BindListData()
        {
            InitializeComponent();
            Validation.AddErrorHandler(this.txtAge, txtAge_ValidationError);
        }
        ICollectionView GetFriendsView()
        {
            Friends people = (Friends)this.FindResource("MyGoodFriends");
            return CollectionViewSource.GetDefaultView(people);
        } 
 

Listing 5

The UI looks like Figure 1.

img1.jpg

Figure 1.

In Figure 1, you can see, we can also move Back and Next. Since data binding is applicable on the controls, we can easily move within the collection of records.

The Birthday button click event handler looks like Listing 6 where we simply gets the current item and find the Age and Name of the friend.

        private void btnBirthday_Click(object sender, RoutedEventArgs e)
        {
            // Get the current person out of the collection view           
            ICollectionView view = GetFriendsView();
            MyFriends person = (MyFriends)view.CurrentItem;
            ++person.Age;
            MessageBox.Show(string.Format("Happy Birthday {0}, on your {1}th Birthday ", person.Name, person.Age));
        }

Listing 6

The output is a MessageBox that looks like Figure 2. 

img2.jpg  

Figure 2. 

The Next and Back button click event handler is listed in Listing 7 and 8 respectively. On these methods, we simply call Next and Back related methods of the view source.

private void btnForward_Click(object sender, RoutedEventArgs e)
{
    ICollectionView view = GetFriendsView();
    view.MoveCurrentToNext();
    if (view.IsCurrentAfterLast)            
    {
        view.MoveCurrentToLast();
    }
}  

Listing 7

img3.jpg 

Figure 3.

private void btnBack_Click(object sender, RoutedEventArgs e)
{
    ICollectionView view = GetFriendsView();
    view.MoveCurrentToPrevious();
    if (view.IsCurrentBeforeFirst) 
   
{
        view.MoveCurrentToFirst();
    }
}

Listing 8

img4.jpg 

Figure 4.  

Now, let's look at some validations. The validation code looks like Listing 9 where we can display an error message and its content. The output looks like Figure 5. 

void txtAge_ValidationError(object sender, ValidationErrorEventArgs e)
{
    // Show the string created in NumberRangeRule.Validate
    //txtAge.ToolTip = (string)e.Error.ErrorContent;
    // Show the string pulled out of the exception by the
    // ExceptionValidationRule
    MessageBox.Show((string)e.Error.ErrorContent, "Validation Error");
}


Listing 9

 img5.jpg

Figure 5. 


namespace System.Windows.Data
{
    public enum UpdateSourceTrigger
    {
        Default = 0, // updates "naturally" based on the target control
        PropertyChanged = 1, // updates the source immediately
        LostFocus = 2, // updates the source when focus changes
        Explicit = 3, // must call BindingExpression.UpdateSource()
    }
}  

Listing 10

Here is an example of conversion of Age.

<class:AgeToForegroundConverter x:Key="ageConverter" />

 img6.jpg 

Figure 6.  

<TextBox  Foreground="{Binding Path=Age, Converter={StaticResource ageConverter}}" Margin="86,69,11,265" x:Name="txtAge">
            <TextBox.Text>
                <Binding Path="Age" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
                    <Binding.ValidationRules>
                        <class:NumberRangeRule Min="0" Max="100"></class:NumberRangeRule>
                    </Binding.ValidationRules>
                </Binding>               
            </TextBox.Text>
</TextBox>

Listing 11

 img7.jpg

Figure 7.

Summary

In this article, I demonstrated how we can use data binding in WPF and use various conversion and data validations in WPF. Please download the attached solution/project to learn more details and anything I have missed in this article.