2

I've a ViewModel class like this in a Prism / WPF project.

public class ContentViewModel : ViewModelBase, IContentViewModel
{
    public ContentViewModel(IPersonService personService)
    {
        Person = personService.GetPerson();
        SaveCommand = new DelegateCommand(Save, CanSave);
    }

    public Person Person { get; set; }

    public DelegateCommand SaveCommand { get; set; }

    private void Save()
    {
        // Save actions here...
    }

    private bool CanSave()
    {
        return Person.Error == null;
    }
}

The person type used in the above ViewModel is defined as follows:

public class Person : INotifyPropertyChanged, IDataErrorInfo
{
    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set
        {
            _firstName = value;
            OnPropertyChanged("FirstName");
        }
    }

    // other properties are implemented in the same way as above...

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    private string _error;
    public string Error
    {
        get
        {
            return _error;
        }
    }

    public string this[string columnName]
    {
        get
        {
            _error = null;
            switch (columnName)
            {
                // logic here to validate columns...
            }
            return _error;
        }
    }
}

An instance of ContentViewModel is set as DataContext of a View. Inside the View I've used binding to Person as follows:

<TextBox Text="{Binding Person.FirstName, ValidatesOnDataErrors=True}" />
<Button Content="Save" Command="{Binding SaveCommand}" />

When I make changes to TextBox which is binded to Person's properties like FirstName and click Save I could see the changes in ViewModel command handler. But if any of these properties fail in validation CanSave is never executed and button never gets disabled.

How do I disable a button based on DelegateCommand's CanExecute action handler in the above scenario?

Phil
  • 39,469
  • 7
  • 92
  • 100
Raj
  • 4,212
  • 11
  • 56
  • 73

3 Answers3

4

In the constructor of ContentViewModel add this line

public ContentViewModel(IPersonService personService)
{
    //GetPerson
    Person.PropertyChanged +=person_PropertyChanged;
}

And write an method to handle that event in which you call either CommandManager.InvalidateRequerySuggested() or SaveCommand.RaiseCanExecuteChanged()

private void person_PropertyChanged(object sender, EventArgs args)
{
   CommandManager.InvalidateRequerySuggested();
   //SaveCommand.RaiseCanExecuteChanged()
}

Hope this works. :-)

  • Issue is that when you update TextBox binding to Person.FirstName Person is never treated as changed and its PropertyChanged event won't be fired. – Raj Mar 14 '12 at 14:34
  • In the setter of the FirstName you are raising a PropertyChanged event with 'FirstName' as argument right? So when the FirstName changes, this gets raised. And in person_PropertyChanged you can check if the argument string is 'FirstName'and do things accordingly – Sreedharlal B Naick Mar 14 '12 at 16:12
  • I've logic in person class to validate its columns - see the indexer in Person class which is implementing IDataErrorInfo. What will be the best way to implement validation without duplicating logic in ViewModel as well? Your suggestion works when I'm ready to implement some kind of validation in ViewModel - in such case IDataErrorInfo is useless. – Raj Mar 15 '12 at 16:18
  • See if this solves you are problem: http://www.arrangeactassert.com/using-idataerrorinfo-for-validation-in-mvvm-with-silverlight-and-wpf/ – Sreedharlal B Naick Mar 19 '12 at 06:25
2

try this with all the properties that can change error:

 public string FirstName
{
    get { return _firstName; }
    set
    {
        _firstName = value;
        OnPropertyChanged("FirstName");

        OnPropertyChanged("Error");
    }
}

Alternatively

        switch (columnName)
        {
            // logic here to validate columns...

            OnPropertyChanged("Error");
        }

The problem you are having is that the OnPropertyChanged is not being called when the error changes.

The next step is to subscribe to the person's propertychanged event when its created, and create a handler that checks for the propertychanged and then changes the boolean variable that the command uses.

 public ContentViewModel(IPersonService personService)
{
    Person = personService.GetPerson();
    Person.PropertyChanged+= PersonPropertyChangedHandler;
    SaveCommand = new DelegateCommand(Save, personHasError);
}

bool personHasError = false;
void PersonPropertyChangedHandler(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    if (e.PropertyName == "Error")
    {
        if(Person.Error == null)
           personHasError = true;
        else
           personHasError = false;
    } 
}

Hope this works. I built this by hand and didn't check it so let me know if its buggy or whatever and ill correct it

Jason Ridge
  • 1,798
  • 14
  • 26
  • The problem you are having is that the OnPropertyChanged is not being called when the error changes. +1 – Raj Mar 15 '12 at 16:36
1

In the nutshell - you should call yourDelegateCommand.RaiseCanExecuteChanged() when you think that your CanExecute() return value can be changed.

In your example, you should notify through INotifyPropertyChanged interface that your Person.Error property is changed, subscribes to Person.PropertyChanged event in your ContentViewModel class and call SaveCommand.RaiseCanExecuteChanged() each time when your Person.Error is changed. Please be careful - in your scenario Person.Error isn't recalculated automatically when, for example, Person.FirstName is changed - you should do this manually.

UPDATED:

public class ContentViewModel : ViewModelBase, IContentViewModel
{
    public ContentViewModel(IPersonService personService)
    {
        Person = personService.GetPerson();
        Person.PropertyChanged += Person_PropertyChanged;
        SaveCommand = new DelegateCommand(Save, CanSave);
    }

    private void PersonPropertyChangedHandler(object sender, PropertyChangedEventArgs e)
    {
        SaveCommand.RaiseCanExecuteChanged();
    }

    private void Save()
    {
        // Save actions here...
    }

    private bool CanSave()
    {
        return IsErrorPresented(Person);
    }

    private bool IsErrorPresented(object o)
    {
        if (!(o is IDataErrorInfo))
            return false;

        var propNames = o.GetType()
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Select(p => p.Name);

        var o2 = (o as IDataErrorInfo);

        var errors = propNames.Select(p => o2[p])
            .Where(p => !String.IsNullOrEmpty(p))
            .ToList();

        ValidationSummary.ErrorMessages = errors;

        return errors.Count > 0;
    }
}

<TextBox Text="{Binding Person.FirstName, 
                        UpdateSourceTrigger=PropertyChanged, 
                        ValidatesOnDataErrors=True, 
                        ValidatesOnExceptions=True, 
                        NotifyOnValidationError=True}" />
<Button Content="Save" Command="{Binding SaveCommand}" />

If you will also specify PropertyChanged as UpdateSourceTrigger, your save button will be updated during your typing..

chopikadze
  • 4,031
  • 23
  • 30
  • You mean something like this private Person _person; public Person Person { get { return _person; } set { _person = value; if (_person.Error != null) SaveCommand.RaiseCanExecuteChanged(); OnPropertyChanged("Person"); } } I dont get any notification in People when it's child change. I think that is the problem here. – Raj Mar 11 '12 at 18:58