Pages

Labels

Tuesday, February 12, 2013

MVVM for Beginners3


Binding the properties of View Model to View.

First and foremost thing is to create the object of PersonViewModel and set it as the DataContext for the Parent Grid in the View.

You can declare the instance of the PersonViewModel in the xaml file by adding reference to the namespace of the class in the xaml and create the object.

<Window x:Class="BegginersMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="250" Width="300"
        xmlns:vm="clr-namespace:BegginersMVVM"
        >
     <Window.Resources>        
        <vm:PersonViewModel x:Key="pvm"/>
</Window.Resources>
.
.
.
</Window>

And set the object created as the DataContext of the Grid.

<Grid DataContext="{StaticResource pvm}">
.
.
.
</Grid>

Now bind the properties of the ViewModel to the controls.
Remember to add  "ValidatesOnDataErrors=True" property for the Textboxes in the binding for the validation to take effect.

<TextBox Text="{Binding Path=MyPerson.FName, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Grid.Row="0" Grid.Column="1" />

Create style for our textboxes to display the ErrorMessage on mouse over.

<Style TargetType="{x:Type TextBox}">
            <Setter Property="HorizontalAlignment" Value="Stretch"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Margin" Value="5"/>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True"> 
                    <Setter Property="ToolTip" Value="{Binding Path=(Validation.Errors)[0].ErrorContent, RelativeSource={x:Static RelativeSource.Self}}" />
                </Trigger>
            </Style.Triggers>
        </Style>

Our final xaml file would look like

<Window x:Class="BegginersMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="250" Width="300"
        xmlns:vm="clr-namespace:BegginersMVVM"
        >
    <Window.Resources>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="HorizontalAlignment" Value="Right"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="HorizontalAlignment" Value="Stretch"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Margin" Value="5"/>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True"> 
                    <Setter Property="ToolTip" Value="{Binding Path=(Validation.Errors)[0].ErrorContent, RelativeSource={x:Static RelativeSource.Self}}" />
                </Trigger>
            </Style.Triggers>
        </Style>
        <vm:PersonViewModel x:Key="pvm"/>
    </Window.Resources>
    <Grid DataContext="{StaticResource pvm}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />            
        </Grid.RowDefinitions>
        <TextBlock Text="First Name:" Grid.Row="0" Grid.Column="0"/>
        <TextBlock Text="Last Name:" Grid.Row="1" Grid.Column="0"/>
        <TextBlock Text="Age:" Grid.Row="2" Grid.Column="0"/>
        <TextBlock Text="Citizenship:" Grid.Row="3" Grid.Column="0"/>
        <TextBox Text="{Binding Path=MyPerson.FName, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Grid.Row="0" Grid.Column="1" />
        <TextBox Text="{Binding Path=MyPerson.LName}" Grid.Row="1" Grid.Column="1"/>
        <TextBox Text="{Binding Path=MyPerson.Age, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Grid.Row="2" Grid.Column="1"/>
        <TextBox Text="{Binding Path=MyPerson.CitizenShip}" Grid.Row="3" Grid.Column="1"/>
        <Button Content="Add" Grid.Row="4" Grid.Column="0" Width="60" Height="30" Grid.ColumnSpan="2"
                Command="{Binding Path=AddCommand}"/>
        
        <ListBox Grid.Column="2" Grid.RowSpan="4" Margin="5" Background="LightCyan"
                 ItemsSource="{Binding Path=Persons}" SelectedItem="{Binding Path=SelectedPerson}" DisplayMemberPath="FName" >
            
        </ListBox>
        <Button Content="Remove" Grid.Row="4" Grid.Column="2" Width="60" Height="30"
                Command="{Binding Path=RemoveCommand}"
                />
        
    </Grid>
</Window>

Points to remember:
1. Model class should not contain the instance of either ViewModel or View.
2. ViewModel should not contain the instance of View.
3. Try to eliminate the code from the codebehind files.

Page1     Page 2     Page3




MVVM for Beginners2


ICommand:
As MVVM strictly condemns from writing code in the codebehind files, ICommand makes it possible to run a particular function on the ViewModel when user clicks on the Button, Hyperlink or MenuItem controls, as these controls have the Command property. We just need to bind our Command object of the ViewModel to the Command property of the control in the UI.

The standard way of creating ICommand object is to inject the commands logic via delegates passed into constructor. RelayCommand class created by the Josh Smith is the best and the easiest way to follow.

private ICommand _addCommand;
public ICommand AddCommand
        {
            get
            {
                if (_addCommand == null)
                    _addCommand = new RelayCommand(param => AddPerson());
                return _addCommand;
            }
        }

IDataErrorInfo:
To provide validations on the data, we need to implement IDataErrorInfo interface on our Model. WPF validation system has the ability to show error messages declared on a property using IDataErrorInfo.

Lets start with our Model which implements IDataErrorInfo.

 public class Person:IDataErrorInfo
    {
        public string FName { get; set; }
        public string LName { get; set; }
        public int Age { get; set; }
        public string CitizenShip { get; set; }
        public string Error
        {
            get { return null; }
        }
        public string this[string columnName]
        {
            get { return ValidateProperty(columnName); }
        }
        private string ValidateProperty(string columnName)
        {
            string errMsg = null;
            if (columnName == "FName")
                if (string.IsNullOrEmpty(FName))
                    errMsg = "First Name is required";
            if (columnName == "Age")
                if (Age <= 0 || Age >= 110)
                    errMsg = "Age should be between 0 and 110";
            return errMsg;
        }
    }

Our business logic has to be implemented in our ViewModel which implements INotifyPropertyChanged and IDataErrorInfo interfaces.
It essentially contains the properties to hold the list of Persons, Person, SelectedPerson from the list and ICommand properties to execute the Add and Remove logic.

 public class PersonViewModel:INotifyPropertyChanged, IDataErrorInfo
    {
        #region Fields
        private Person _myPerson;
        private ObservableCollection<Person> _persons;
        private Person _selectedPerson;
        private ICommand _addCommand;
        private ICommand _removeCommand; 
        #endregion
        #region Constructor
        public PersonViewModel()
        {
            Persons = new ObservableCollection<Person>();
            MyPerson = new Person();
        } 
        #endregion
       
        #region Properties
        public Person MyPerson
        {
            get { return _myPerson; }
            set { _myPerson = value; this.OnPropertyChanged("MyPerson"); }
        }
        //Collection to hold the persons list.
        public ObservableCollection<Person> Persons
        {
            get { return _persons; }
            set { _persons = value; }
        }
        
        //property to hold the selected person from the list
        public Person SelectedPerson
        {
            get { return _selectedPerson; }
            set { _selectedPerson = value; this.OnPropertyChanged("SelectedPerson"); }
        }
        public ICommand AddCommand
        {
            get
            {
                if (_addCommand == null)
                    _addCommand = new RelayCommand(param => AddPerson());
                return _addCommand;
            }
        }
        public ICommand RemoveCommand
        {
            get
            {
                if (_removeCommand == null)
                {
                    _removeCommand = new RelayCommand(param => RemovePerson(), param => CanRemovePerson());
                }
                return _removeCommand;
            }
        } 
        #endregion
        #region Methods
        
        //To check whether a person can be removed from the list. Enables/disables the Remove button based on selection
        private bool CanRemovePerson()
        {
            return this.SelectedPerson != null;
        }
        
        //To remove the selected person from the list
        private void RemovePerson()
        {
            if (SelectedPerson != null)
                this.Persons.Remove(this.SelectedPerson);
        }
       
        //logic to add the person to the Persons collection.
        private void AddPerson()
        {
            Persons.Add(this.MyPerson);
            MyPerson = new Person();
        }
        #endregion
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string propName)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propName));
            }
        } 
        #endregion
        #region IDataErrorInfo
        public string Error
        {
            get { return (MyPerson as IDataErrorInfo).Error; }
        }
        public string this[string columnName]
        {
            get { return ValidateProperties(columnName); }
        }
        private string ValidateProperties(string columnName)
        {
            string errMsg = null;
            if (columnName == "MyPerson")
            {
                errMsg = (MyPerson as IDataErrorInfo).Error;
            }
            return errMsg;
        } 
        #endregion
    }


Page1     Page2     Page3