I am new to the WPF MVVM and wanted to ask a follow up question to this article:
Enable Disable save button during Validation using IDataErrorInfo
I am trying to enable/disable the button save/update if any of the many controls on the form validation failed/passed.
I have the IsValid method, that checks the validation logic on the Model and returns True/False, that will be passed on to the DelegateCommand as a predicate.
The question is: my button has the following property IsEnabled{binding IsValid}, this should check all fields to make sure that it matches the criteria in the model, returns true/false to the view model and then enables the button if all true. The problem is: Once the view model is instantiated, the DelegateCommand object is created with the validation (IsValid) at at a false state and it stays that way throughout the life of the object even though the user is filling data in the textboxes. How do I turn on the button once all the conditions are met? in other words, how to constantly keep validating and updating the IsValid in order to switch the button on once every textbox validate to true??
Thanks,
I have the following code:
The Model
public class UserModel : ObservePropertyChanged,IDataErrorInfo
{
private string name;
public string Name
{
get { return name; }
set { name = value; OnPropertyChanged("Name"); }
}
// if there is an error throw an exception
public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string columnName]
{
get
{
string result = null;
if (columnName == "Name")
{
if (string.IsNullOrEmpty(Name))
result = "Please enter a Name";
}
return result;
}
}
// the Josh Way
static readonly string[] ValidatedProperties =
{
"Name"
};
public bool IsValid
{
get
{
foreach (string property in ValidatedProperties)
{
if (GetValidationError(property) != null) // there is an error
return false;
}
return true;
}
}
// a method that checks validation error
private string GetValidationError(string propertyName)
{
string error = null;
switch (propertyName)
{
case "Name":
error = this.ValidateName();
break;
default:
error = null;
throw new Exception("Unexpected property being validated on Service");
}
return error;
}
private string ValidateName()
{
string ErrorMsg = null;
if (string.IsNullOrWhiteSpace(Name))
{
ErrorMsg = "Name can't be empty!";
};
return ErrorMsg;
}
}
** the View Model **
public class UserViewModel:ObservePropertyChanged
{
UserModel model;
public UserViewModel()
{
presentCommand = new DelegateCommand(param => PresentDataMethod(), param => CanSave);
model = new UserModel();
}
private string name;
public string Name
{
get { return name; }
set { name = value; OnPropertyChanged("Name"); }
}
private string info;
public string Info
{
get { return info; }
set { info = value; OnPropertyChanged("Info"); }
}
private DelegateCommand presentCommand;
public DelegateCommand PresentCommand
{
get
{
if (presentCommand==null)
{
presentCommand = new DelegateCommand(param => PresentDataMethod(), param => CanSave);
}
return presentCommand;
}
}
private void PresentDataMethod()
{
Info = $"Your Name is: {Name}.";
}
// The ViewModel then contains a CanSave Property that reads the IsValid property on the Model:
protected bool CanSave
{
get
{
return model.IsValid;
}
}
}
** The View**
<TextBox x:Name="Name" HorizontalAlignment="Left" Height="34" Margin="285,145,0,0" TextWrapping="Wrap"
VerticalAlignment="Top" Width="248">
<Binding Path="Name"
ValidatesOnDataErrors="True"
UpdateSourceTrigger="PropertyChanged"
Mode="TwoWay">
</Binding>
</TextBox>
<Button Content="Present" FontSize="20" HorizontalAlignment="Left"
Margin="285,184,0,0" VerticalAlignment="Top" Width="248" Height="35"
Command="{Binding Path=PresentCommand}"
IsEnabled="{Binding IsValid}"
>
</Button>
If all you want to do is refresh the button via the IsValid value, all you have to do is listen for any of the OTHER property changes in your ViewModel, and when that happens, tell it to refresh the IsValid binding as well (which is actually your CanSave property). Here is one way to do that:
** The View Model **
// ...
public UserViewModel()
{
// ...
this.PropertyChanged += OnViewModelPropertyChanged;
}
public void OnViewModelPropertyChanged(object sender, PropertyEventArgs e)
{
// On any property that implements "OnPropertyChanged(propname)", refresh the CanSave binding too!
OnPropertyChanged(nameof(this.CanSave));
}
// ...
BTW, it is generally good coding practice to avoid magic words like OnPropertyChanged("Name") or OnPropertyChanged("Info") because you never know when a developer will have to rename their properties, and if that happens, you won't get a compile error here and it might be hard to debug. It is best practice to use the nameof, such as OnPropertyChanged(nameof(Name)) so that you'll know you'll get a compilation error if you ever decide to change the property to, say, FirstName instead of Name.
Related
I have a popup view with x:Name=This, on it a button delcared as follows:
<Button Content="Log in" Command="{Binding Path=LoginCommand}" CommandParameter="{Binding ElementName=This}" />
This is to gain access to the non-bindable Password property, which is a SecureString type.
In my ctor I initialise the command like so:
public LoginPopupViewModel()
{
LoginCommand = new DelegateCommand<IHavePassword>(
LogUserIn,
p => !string.IsNullOrWhiteSpace(Username));
}
I fully expect that when I type something in the Username, and change focus, the property change notification will help enable the Login button. It doesn't, so I have added the extra code, and the button still remains disabled.
public string Username
{
get { return _username; }
set
{
if (value == _username) return;
_username = value;
OnPropertyChanged();
CommandManager.InvalidateRequerySuggested();
}
}
If I change the CanExecute delegate like below, only then is the button enabled:
public LoginPopupViewModel()
{
LoginCommand = new DelegateCommand<IHavePassword>(
LogUserIn,
p => true);
}
Why does this button remain disabled even when its command can execute?
I have tried a sample program and binding seems to work fine. I don't have your complete source code but you need to use RaiseCanExecuteChanged on the delegate command when you want the command to check if it needs to execute. Have you checked if the binding on the username is correct?
this.loginCommand.RaiseCanExecuteChanged(); is the key to the answer
public LoginPopupViewModel()
{
this.loginCommand = new DelegateCommand(() =>
{
MessageBox.Show("Logged In Click");
}, () =>
{
return !string.IsNullOrEmpty(UserName);
});
}
private DelegateCommand loginCommand;
private string userName;
public ICommand LoginCommand
{
get { return loginCommand; }
}
public string UserName
{
get { return this.userName; }
set
{
if (value == this.userName)
{
return;
}
this.userName = value;
OnPropertyChanged("UserName");
this.loginCommand.RaiseCanExecuteChanged();
}
}
public string Password { get; set; }
I have a WPF ComboBox bound to a list of a class which contains an enum.
This all works fine, my question is at the end of this post, first the code:
Here is the class:
public class FILTER_TEST
{
public FilterType Filter { get; private set; }
public string Description { get; private set; }
public static List<FILTER_TEST> CreateFilters()
{
var list = new List<FILTER_TEST>();
list.Add(new FILTER_TEST() { Filter = FilterType.CheckNone, Description = "Uncheck all" });
list.Add(new FILTER_TEST() { Filter = FilterType.CheckAll, Description = "Check all" });
list.Add(new FILTER_TEST() { Filter = FilterType.CheckCustom, Description = "Custom check" });
return list;
}
}
Here is the enum FilterType:
public enum FilterType
{
CheckNone,
CheckAll,
CheckCustom
}
In my view model I have the following:
public List<FILTER_TEST> FilterNames { get { return FILTER_TEST.CreateFilters(); } }
public FILTER_TEST SelectedFilter
{
get { return selectedFilter; }
set
{
if (value != selectedFilter)
{
selectedFilter = value;
OnPropertyChanged("SelectedFilter");
}
}
}
Also in the view model, I set the SelectedItem of the ComboBox as follows:
SelectedFilter = FilterNames.Where(x => x.Filter == FilterType.CheckNone).FirstOrDefault();
Here is the xaml putting it all together:
<ComboBox DisplayMemberPath="Description" ItemsSource="{Binding FilterNames}"
SelectedItem="{Binding SelectedFilter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="True"/>
My problem is that although the changing of the SelectionItem works, the actual value displayed in the ComboBox doesn’t change.
The initial SelectedItem is “Uncheck all” as, when the window has been loaded, none of the corresponding CheckBox controls (bound to another class which contains a Boolean property) have been checked. What I would like is that when a CheckBox has been checked, then the SelectedItem changes to “Custom check”.
This does indeed change the value of the SelectedItem:
SelectedFilter = FilterNames.Where(x => x.Filter == FilterType.CheckCustom).FirstOrDefault();
But the text shown in the ComboBox is still “Uncheck all”.
Does anyone have an idea as to what I am missing? I am forced to use the 4.0 framework, I don’t know if this is relevant.
I've seen the hint to overwrite Equals() of the type in use as this:
public override bool Equals(object o)
{
if (o is FILTER_TEST)
{
var other = o as FILTER_TEST;
return this.Description == other.Description && this.Filter == other.Filter;
}
else
return false;
}
Now that makes your sample work. Let me come back for a reference on the why.
This seemed so simple but has turned into a nightmare for me. Everything works great, i can select a value and it's reported back to the view model.
Problem:
User opens the settings flyout and selects a value. User exits the flyout.
User reopens the settings flyout and there is no selected value in the combobox. The value exists in the view model though.
Scenario:
Combobox in a Settingsflyout.
<ComboBox x:Name="defaultComboBox" SelectedItem="{Binding UserSettings.DefaultAccount, Mode=TwoWay}" ItemsSource="{Binding UserAccounts}" DisplayMemberPath="CustomName">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Loaded">
<core:InvokeCommandAction Command="{Binding UserAccountComboboxLoadedCommand}" CommandParameter="{Binding ElementName=defaultAccountComboBox}"/>
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</ComboBox>
ViewModelCode:
public void Open(object parameter, Action successAction)
{
logger.LogProgress("Opened UserSettingsFlyoutView.");
UserSettings.DefaultAccount = UserAccounts.FirstOrDefault(u => u.AccountID.ToString().Equals(userSettings.DefaultAccountGuid,StringComparison.CurrentCultureIgnoreCase));
}
public CrossThreadObservableCollection<UserAccount> UserAccounts
{
get
{
try
{
return dbContext.RetrieveAllUserAccounts();
}
catch(Exception e)
{
logger.LogError("Error happened when retrieving user-accounts from secure data store Error: " + e.Message, e.ToString());
return new CrossThreadObservableCollection<UserAccount>();
}
}
}
private IProvideUserSetting userSettings;
public IProvideUserSetting UserSettings
{
get { return userSettings; }
set { userSettings = value; OnPropertyChanged("UserSettings"); }
}
UserSettings class:
private string defaultAccountGuid;
[DataMember]
public string DefaultAccountGuid
{
get { return defaultAccountGuid; }
set { defaultAccountGuid = value; OnPropertyChanged("DefaultAccountGuid"); }
}
private UserAccount defaultAccount;
[IgnoreDataMember]
public UserAccount DefaultAccount
{
get { return defaultAccount; }
set {
defaultAccount = value;
if (defaultAccount != null)
DefaultAccountGuid = defaultAccount.AccountID.ToString();
OnPropertyChanged("DefaultAccount"); }
}
I tried a version of the code and could not reproduce the issue. Could you provide more code? Is there something else setting the selected item?
Anyway, the type of item in the ItemsSource is different than the type of item for selected item. I would try changing the selected item binding to the same class in the items source.
For example, instead of the viewmodel property UserSettings, make that object type UserAccount.
Something like
private UserAccount _selectedUserAccount { get; set; }
public UserAccount SelectedUserAccount
{
get { return _selectedUserAccount; }
set
{
if (_selectedUserAccount != value)
{
_selectedUserAccount = value;
OnPropertyChanged("SelectedUserAccount");
}
}
}
Edit:
You can add a loaded event handler to your combobox, then locate the viewmodel from the code behind and set the selected item property.
private void ComboBox_Loaded(object sender, RoutedEventArgs e)
{
ComboBox comboBox = sender as ComboBox;
comboBox.SelectedItem =
_viewModel.UserAccounts.Where(x => x.UserAccountString == _viewModel.SelectedUserAccount.UserAccountString);
}
I have applied the validations of required field validation on the wpf textbox, using the ValidationRule class. My code is :
public class RequiredField : ValidationRule
{
private String _errorMessage = String.Empty;
public string ErrorMessage
{
get { return _errorMessage; }
set { _errorMessage = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var str = value as string;
if (String.IsNullOrEmpty(str))
{
return new ValidationResult(false, this.ErrorMessage);
}
return new ValidationResult(true, null);
}
}
And the XAML code is below :
<TextBox Name="txtName" MaxLength="50">
<TextBox.Text>
<Binding Path="Name" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<myValidtaion:RequiredField ErrorMessage="Please enter Name." />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
This seems to work fine. But the issue is that after i save the valid data in database and clear the database, this validation is fired again and validation message appears again.
How can I avoid this situation ?
This seems to be a common validation 'problem', but it is in fact the expected behviour of WPF validation... this also occurs when using the IDataErrorInfo interface.
Both of these validation methods will validate their specified properties whenever their values change, which is exactly what we want (most of the time).
I believe that the system is called predictive validation and it enables us to show the user what they have to fill in before they try to save. I personally believe that this is a better system than the old system of letting the user try to save fields with errors and then telling them afterwards that they have made an error.
Perhaps you can delay clearing the fields until the user clicks a New button, in which case the errors will only show up then?
UPDATE >>>
If you were to use the IDataErrorInfo interface, then you could add a bool IsValidating property to your data type classes that you could use to switch validation on and off. I don't have much time, so I found a post that implements this interface that I have adapted to demonstrate my point.
public class Person : IDataErrorInfo
{
private int age;
private bool isValidating = true;
public int Age
{
get { return age; }
set { age = value; }
}
public bool IsValidating
{
get { return isValidating; }
set { isValidating = value; }
}
public string Error
{
get
{
return this["Age"];
}
}
public string this[string name]
{
get
{
string result = null;
if (IsValidating)
{
if (name == "Age")
{
if (this.age < 0 || this.age > 150)
{
result = "Age must not be less than 0 or greater than 150.";
}
}
}
return result;
}
}
}
Adapted from the code in the accepted answer to Creating WPF Validation error manually post
When you switch this IsValidating property to false, the property changes won't be validated:
person.IsValidating = false;
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?
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. :-)
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
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..