WPF button content is empty when shown - c#

I am new to C#/WPF. There is a view with one button defined, when the view is initialized, buttons will display a set of reason codes got from DataContext (viewmodel), once any button is clicked, the code on it will be saved and passed forward for next processing.
Q: The text on buttons are totally empty, but the clicked code can be captured, so where the problem is about binding? Thanks.
XAML:
<Button x:Name="btnReason" Command="{Binding DataContext.SelectCommand, RelativeSource={RelativeSource AncestorType=v:View, Mode=FindAncestor}}" CommandParameter="{Binding}" Width="190" Height="190" >
<Border Background="Transparent">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock x:Name="Reason" Grid.Row="0" Text="{Binding ?????}" TextWrapping="Wrap" />
</Grid>
</Border>
</Button>
The code on C#:
public class ReasonsViewModel : ViewModel
{
private IEnumerable<string> m_Names;
public IEnumerable<string> Names
{
get { return m_Names; }
set
{
if (m_Names != value)
{
m_Names = value;
OnPropertyChanged(() => Names);
}
}
}
private string m_SelectedName;
public string SelectedName
{
get { return m_SelectedName; }
set
{
if (m_SelectedName != value)
{
m_SelectedName = value;
OnPropertyChanged(() => SelectedName);
}
}
}
public DelegateCommand SelectCommand { get; private set; }
public ReasonsViewModel()
{
SelectCommand = new DelegateCommand(p => SelectCommandExecute(p));
}
private bool m_Processing;
private void SelectCommandExecute(object item)
{
if (m_Processing) return;
try
{
m_Processing = true;
var name = item as string;
if (name == null) return;
SelectedName = name;
}
finally
{
m_Processing = false;
}
}
}

If I understood your question correctly than your property text in your TextBlock should be bound to SelectedName.
The problem is that your CommandParameter is bound to DataContext. That's what an empty {Binding} statement bounds to. This means your command handler always returns after the null check.
I also suggest that you change your Names proeprty from IEnumerable<string> to ObservableCollection<string>.
ObservableCollection raises events on any additions or removalof items inside and WPF components can bind to these events.

Related

Edit Record. Pass values of bound TextBoxes to View Model

I have a DataGrid which is bound to a ViewModel. When I select a record from the DataGrid, the TextBoxes (Username and Role) are displaying the data from the selected record.
I want to edit the selected record but I'd like to check the data before it updates the list, hence the 'OneWay' binding mode.
I'm having trouble passing the values of the textboxes to the view model. I can get a value of one textboxes through the button and passing the value to my ICommand
<Button Grid.Row="5" Grid.Column="1" Content="Edit" Margin="5 5"
Command="{Binding EditUserCmd, Source={StaticResource viewModelUsers}}" CommandParameter="{Binding Text, ElementName=txtUsername}
Is there a way to pass all the textboxes to the view model by creating a property in it that holds selected user? or passing the values of the texboxes to the view model somehow??
Thanks.
My view model
public class UsersViewModel
{
public ObservableCollection<UsersModel> Users { get; set; }
private ICommand addUserCommand;
private ICommand removeUserCommand;
private ICommand editUserCommand;
public ICommand AddUserCmd => addUserCommand ?? (addUserCommand = new AddUserCommand(this));
public ICommand RemoveUserCmd => removeUserCommand ?? (removeUserCommand = new DeleteUserCommand(this));
public ICommand EditUserCmd => editUserCommand ?? (editUserCommand = new EditUserCommand(this));
private UsersModel selectedUser = new UsersModel();
public UsersModel SelectedUser
{
get { return this.selectedUser; }
set
{
this.selectedUser = value;
}
}
public UsersViewModel()
{
// fetch data from db.
DataAccess da = new DataAccess();
Users = new ObservableCollection<UsersModel>(da.GetRegisteredUsers());
}
}
Model
public class UsersModel
{
public int Id { get; set; }
public string Username { get; set; }
public string Surname {get; set;}
}
Edit Command
internal class EditUserCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public UsersViewModel UsersViewModel { get; set; }
public EditUserCommand(UsersViewModel usersViewModel)
{
this.UsersViewModel = usersViewModel;
}
public bool CanExecute(object parameter)
{
// UsersModel user = (UsersModel)parameter;
// if (user != null)
//return !string.IsNullOrEmpty(user.Id.ToString());
return true;
}
public void Execute(object parameter)
{
// UsersModel user = (UsersModel)parameter;
// if (user != null)
// this.UsersViewModel.Users
}
}
xaml
...
<Window.Resources>[enter image description here][1]
<m:UsersModel x:Key="users"></m:UsersModel>
<vm:UsersViewModel x:Key="viewModelUsers"/>
</Windows.Resources>
...
<DataGrid x:Name="gridUsers"
Grid.Row="0"
DataContext="{Binding Source={StaticResource viewModelUsers}}" CanUserAddRows="False"
ItemsSource="{Binding Users}">
</DataGrid>
<Grid Margin="10" Grid.Row="1" DataContext="{Binding ElementName=gridUsers, Path=SelectedItem}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0">UserName:</Label>
<TextBox x:Name="txtUsername" Grid.Row="0" Grid.Column="1" Margin="0,0,0,10" Text="{Binding Path=Username, Mode=OneWay}"/>
<Label Grid.Row="1">Role:</Label>
<TextBox x:Name="txtRole" Grid.Row="1" Grid.Column="1" Margin="0,0,0,10" Text="{Binding Path=Role, Mode=OneWay}"/>
<StackPanel Grid.Row="5" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Center">
<Button Grid.Row="5" Grid.Column="1" Content="Edit" Margin="5 5"
Command="{Binding EditUserCmd, Source={StaticResource viewModelUsers}}" CommandParameter="{Binding Text, ElementName=txtUsername}">
</StackPanel>
</Grid>
Your ViewModel should not know about text boxes - just add two new properties ([PropertyName]EditValue) and bind to them, then in your command check them and copy them to the model if correct or restore them if incorrect - This is the entire point in using view models instead of binding to models directly
Did you know that you can edit the DataGrid cells directly? You can even use data validation. This way invalid data cells get a red border and the data won't be committed unless the validation passes.
Another option is to let UsersModel implement INotifyDataErrorInfo and validate properties directly. Then bind the DataGrid.SelectedItem to the view model and bind the edit TextBox elements to this property. This way you implemented live update and got rid of the edit commands:
UsersViewModel.cs
public class UsersViewModel
{
public ObservableCollection<UsersModel> Users { get; set; }
private UsersModel selectedUser;
public UsersModel SelectedUser
{
get => this.selectedUser;
set => this.selectedUser = value;
}
public UsersViewModel()
{
// fetch data from db.
DataAccess da = new DataAccess();
Users = new ObservableCollection<UsersModel>(da.GetRegisteredUsers());
}
}
UsersModel.cs
public class UsersModel : INotifyDataErrorInfo
{
private int id;
public int Id
{
get => this.id;
set { if (this.id != value && IsIdValid(value)) this.id = value; }
}
private string userName;
public string UserName
{
get => this.userName;
set { if (this.userName != value && IsUserNameValid(value) && ) this.userName = value; }
}
private string surname;
public string Surname
{
get => this.surname;
set { if (this.surname != value && IsSurnameValid(value) && ) this.surname = value; }
}
// Validates the Id property, updating the errors collection as needed.
public bool IsIdValid(int value)
{
RemoveError(nameof(this.Id), ID_ERROR);
if (value < 0)
{
AddError(nameof(this.Id), ID_ERROR, false);
return false;
}
return true;
}
public bool IsUserNameValid(string value)
{
RemoveError(nameof(this.UserName), USER_NAME_ERROR);
if (string.IsNullOrWhiteSpace(value))
{
AddError(nameof(this.UserName), USER_NAME_ERROR, false);
return false;
}
return true;
}
public bool IsSurnameValid(string value)
{
RemoveError(nameof(this.Surname), SURNAME_ERROR);
if (string.IsNullOrWhiteSpace(value))
{
AddError(nameof(this.Surname), SURNAME_ERROR, false);
return false;
}
return true;
}
private Dictionary<String, List<String>> errors =
new Dictionary<string, List<string>>();
private const string ID_ERROR = "Value cannot be less than 0.";
private const string USER_NAME_ERROR = "Value cannot be empty.";
private const string SURNAME_ERROR = "Value cannot be empty.";
// Adds the specified error to the errors collection if it is not
// already present, inserting it in the first position if isWarning is
// false. Raises the ErrorsChanged event if the collection changes.
public void AddError(string propertyName, string error, bool isWarning)
{
if (!errors.ContainsKey(propertyName))
errors[propertyName] = new List<string>();
if (!errors[propertyName].Contains(error))
{
if (isWarning) errors[propertyName].Add(error);
else errors[propertyName].Insert(0, error);
RaiseErrorsChanged(propertyName);
}
}
// Removes the specified error from the errors collection if it is
// present. Raises the ErrorsChanged event if the collection changes.
public void RemoveError(string propertyName, string error)
{
if (errors.ContainsKey(propertyName) &&
errors[propertyName].Contains(error))
{
errors[propertyName].Remove(error);
if (errors[propertyName].Count == 0) errors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
}
public void RaiseErrorsChanged(string propertyName)
{
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
#region INotifyDataErrorInfo Members
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (String.IsNullOrEmpty(propertyName) ||
!errors.ContainsKey(propertyName)) return null;
return errors[propertyName];
}
public bool HasErrors
{
get => errors.Count > 0;
}
#endregion
}
View
TextBox.Text bindings must be set to TwoWay (which is the default Binding.Mode value for this property)
<DataGrid x:Name="gridUsers"
DataContext="{Binding Source={StaticResource viewModelUsers}}"
CanUserAddRows="False"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser"} />
<Grid DataContext="{Binding Source={StaticResource viewModelUsers}, Path=SelectedUser}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0">UserName:</Label>
<TextBox x:Name="txtUsername" Grid.Row="0" Grid.Column="1"
Text="{Binding Username, NotifyOnValidationError=True"/>
<Label Grid.Row="1">Role:</Label>
<TextBox x:Name="txtRole" Grid.Row="1" Grid.Column="1"
Text="{Binding Role, NotifyOnValidationError=True}"/>
</Grid>

UI not updating despite ObservableCollection (UWP, XAML) [duplicate]

This question already has answers here:
ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)
(21 answers)
Closed 6 years ago.
I would greatly appreciate some help with this binding issue I'm having. Basically I have a list view showing some information about Files. In the list view item itself, there's some text and also a button.
When this button is clicked I want to disable that button.
Currently I've set up an ObservableCollection - however even though the button click is being registered, the UI doesn't update. If I go to a different screen and return, then the UI updates. So it's not instantaneous.
I think there is some problem with the way RaisePropertyChanged() is working. I know from reading other SO articles that property changes in the object are harder to pick up than say, removing an item or adding an item to the ListView.
I'm completely stuck, any help would be most appreciated. Thanks.
Xaml:
<ListView RelativePanel.Below="heading" ItemsSource="{Binding Pages}" ReorderMode="Enabled" CanReorderItems="True" AllowDrop="True" Margin="0,10" SelectedItem="{Binding Path=SelectedFile,Mode=TwoWay}" >
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:File">
<Grid Padding="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{x:Bind Path= Name, Mode=TwoWay}" FontWeight="Bold" Padding="0,5" />
<TextBlock Text ="{x:Bind Path}" Grid.Row="1" TextWrapping="Wrap" Padding="10,0,0,0" Foreground="DarkGray" Opacity="0.8" />
<Button Content="X" Grid.Column="1" Grid.RowSpan="2" Command="{x:Bind EnableCommand}" IsEnabled="{x:Bind Path=IsEnabled, Mode=OneWay}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
File.cs:
public class File : ViewModelBase
{
public string Name { get; set; }
public string FileName { get; set; }
public string Path { get; set; }
public string Contents { get; set; }
private Boolean isEnabled = true;
public Boolean IsEnabled {
get { return isEnabled; }
private set {
isEnabled = value;
RaisePropertyChanged("IsChecked");
}
}
private ICommand enableCommand;
public ICommand EnableCommand
{
get
{
if(enableCommand == null)
{
enableCommand = new RelayCommand(() => {
isEnabled = false;
Name += "Disabled";
RaisePropertyChanged();
});
}
return enableCommand;
}
}
}
Viewmodel:
public class MyPageViewModel : BaseViewModel
{
private ObservableCollection<File> pages;
public ObservableCollection<File> Pages
{
get { return pages; }
set
{
pages = value;
RaisePropertyChanged();
}
}
private File selectedFile = new File();
public File SelectedFile
{
get { return selectedFile; }
set
{
Set(ref selectedFile, value);
}
}
public MyPageViewModel()
{
if (ApplicationData.FileList != null)
{
Pages = new ObservableCollection<File>(ApplicationData.FileList);
}
else
{
Pages = new ObservableCollection<File>();
}
}
You notify IsChecked when you should be notifying IsEnabled.
(ObsevarvableCollection only notifies when something is added or removed from it. Changes in the objects it holds are not notified by it.)

Bind combo box within listbox WPF

I am new-bee at WPF, i am trying to populate my combox control which is there within my listbox
XAML :
<Window.Resources>
<DataTemplate x:Key="UserTemplate" >
<StackPanel Orientation="Horizontal" >
<ComboBox Name="rule" ItemsSource="{Binding}" DisplayMemberPath="DataContext.RuleType" Width="85" Height="20"
SelectedValuePath="DataContext.RuleType" SelectedValue="{Binding Path=DataContext.RuleType}"/>
<TextBlock Text="{Binding Path= Name1}" Width="85" Margin="5,5,5,5"></TextBlock>
<Button Content="Delete" Click="cmdDeleteUser_Clicked" Margin="5,5,5,5" />
<Button Content="Add" Click="cmdAddUser_Clicked" Margin="5,5,5,5" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox Name="lbUsers" ItemsSource="{Binding }" ItemTemplate="{StaticResource UserTemplate}"/>
</Grid>
CODE BEHIND:
public ObservableCollection<User> Users;
ObservableCollection<Listdata> listeddata;
ObservableCollection<Records> Record;
public MainWindow()
{
InitializeComponent();
Users = new ObservableCollection<User>() {
new User() { Name = "", Age = "" },
};
DataboundListbox.Records record = new Records();
RuleType = record.record_Rule();
lbUsers.DataContext = Users;
}
private string _Name;
public string Name1
{
get { return _Name; }
set
{
if (value != _Name)
{
_Name = "John";
NotifyPropertyChanged("Name");
}
}
}
private List<string> _RuleType;
public List<string> RuleType
{
get { return _RuleType; }
set
{
if (value != _RuleType)
{
_RuleType = value;
NotifyPropertyChanged("RuleType");
}
}
}
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private void cmdDeleteUser_Clicked(object sender, RoutedEventArgs e)
{
Button cmd = (Button)sender;
if (cmd.DataContext is User)
{
User deleteme = (User)cmd.DataContext;
Users.Remove(deleteme);
}
}
private void cmdAddUser_Clicked(object sender, RoutedEventArgs e)
{
Button cmd = (Button)sender;
if (cmd.DataContext is User)
{
var addedUser = new User() { Name = "", Age = "" };
Users.Add(addedUser);
}
}
private List<string> _prp;
public List<string> prp
{
get { return _prp; }
set
{
if (value != _prp)
{
_RuleType = value;
NotifyPropertyChanged("prp");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
Before I can answer your question there are some confusions that should be cleared up.
If User has already a member named Name then what's Name1 in parent class for?
If RuleType is a list, how come it's set as the SelectedValue of your ComboBox, Shouldn't it be ComboBox.itemsSource instead? If it should, then where is the property defined to keep the ComboBox.SelectedValue?
How come there is an Add button inside the UserTemplate? Delete button is ok but i think Add belongs outside of the ListBox.
If i understand your issue correctly, then this is the solution I can think of.
Fisrt: User needs a property like SelectedRule to keep Combobox.SelectedItem:
public class User : INotifyPropertyChanged
{
// implementation of INotifyPropertyChanged
string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
NotifyPropertyChanged("Name");
}
}
int _age;
public int Age
{
get
{
return _age;
}
set
{
_age = value;
NotifyPropertyChanged("Age");
}
}
string _selectedRule;
public string SelectedRule
{
get
{
return _selectedRule;
}
set
{
_selectedRule = value;
NotifyPropertyChanged("SelectedRule");
}
}
}
Second: Your DataTemplate should change like this:
<Window.Resources>
<DataTemplate x:Key="UserTemplate" >
<StackPanel Orientation="Horizontal" >
<ComboBox Name="rule" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=RuleType}" DisplayMemberPath="." Width="85" Height="20"
SelectedItem="{Binding SelectedRule}"/>
<TextBlock Text="{Binding Path= Name}" Width="85" Margin="5,5,5,5"></TextBlock>
<Button Content="Delete" Click="cmdDeleteUser_Clicked" Margin="5,5,5,5" />
</StackPanel>
</DataTemplate>
</Window.Resources>
Finally the ListBox part changes as below:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" Name="lbUsers" ItemsSource="{Binding}" ItemTemplate="{StaticResource UserTemplate}"/>
<Button Grid.Row="1" Content="Add" Click="cmdAddUser_Clicked" Margin="5,5,5,5" />
</Grid>
If you're gonna bring Add button out like the above code, then you should remove if (cmd.DataContext is User) from cmdAddUser_Clicked method.
Problem :
The main problem is on this two line:
{Binding Path=DataContext.RuleType}
{Binding Path= Name1}
Since you already declare your dataContext, DataContext.RuleType will causes the compiler to search for yourdatacontext.DataContext.RuleType which is obviously not the thing you want.
lbUsers.DataContext = Users;
Your data context is a collection of User class and does not contain Name1. Thus Binding Path=Name1 will return "property not found" error
Solution
In WPF, MVVM ( model view viewmodel) pattern is highly encouraged. One of its main feature is it seperate GUI logic from Business Logic, making the code cleaner and easier to maintain.
Step 1: Create a ViewModel
public class UserViewModel:INotifyPropertyChanged
{
private string name;
private string age;
private string rule;
private List<string> ruleType;
public String Name
{
get { return name; }
set { name = value; NotifyPropertyChanged("Name"); }
}
public String Age
{
get { return age; }
set { age = value; NotifyPropertyChanged("Age"); }
}
public String Rule
{
get { return rule; }
set { rule = value; NotifyPropertyChanged("Rule"); }
}
public List<string> RuleType
{
get { return ruleType; }
set { ruleType = value; NotifyPropertyChanged("RuleType"); }
}
public UserViewModel()
{
name = "name";
age = "";
ruleType = new List<string>();
}
#region NotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
}
Step 2 : Link your data context to the viewmodel
public MainWindow()
{
InitializeComponent();
Users = new ObservableCollection<UserViewModel>();
//setup your data here
//example:
UserViewModel userViewModel = new UserViewModel();
//populate your combobox here
userViewModel.RuleType.Add("rule1")
userViewModel.RuleType.Add("rule2");
userViewModel.RuleType.Add("rule3");
Users.Add(new UserViewModel());
lbUsers.DataContext = Users ;
}
Step 3 : Update your xaml
<Window.Resources>
<DataTemplate x:Key="UserTemplate" >
<StackPanel Orientation="Horizontal" >
<ComboBox Name="rule" ItemsSource="{Binding RuleType}" Width="85" Height="20"
SelectedValue="{Binding Rule}"/>
<TextBlock Text="{Binding Path= Name}" Width="85" Margin="5,5,5,5"></TextBlock>
<Button Content="Delete" Click="cmdDeleteUser_Clicked" Margin="5,5,5,5" />
<Button Content="Add" Click="cmdAddUser_Clicked" Margin="5,5,5,5" />
</StackPanel>
</DataTemplate>
</Window.Resources>
When i am typing, bahman already post a quite detailed answer.So i stopped here. If you require any explaination or solution from me just asked will do.
In future if you suspect any error regarding binding, you can search your output window.
If you see your output window you possibly will found this
System.Windows.Data Error: 40 : BindingExpression path error: 'DataContext' property not found on 'object' ''User' (HashCode=9080996)'. BindingExpression:Path=DataContext.RuleType; DataItem='User' (HashCode=9080996); target element is 'ComboBox' (Name=''); target property is 'SelectedValue' (type 'Object')
System.Windows.Data Error: 40 : BindingExpression path error: 'Name1' property not found on 'object' ''User' (HashCode=9080996)'. BindingExpression:Path=Name1; DataItem='User' (HashCode=9080996); target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')

update style of selected item in tow way mode

i have a view that have a list view with data template
i need to set style on the selected item
but i need also when the selected item is been changed from the code it modify the selected item in the view
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" Width="300" Height="50" TextAlignment="Center"/>
<ListView Grid.Row="1" ItemsSource="{Binding List, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ListView.ItemTemplate>
<DataTemplate >
<Border BorderThickness="1" BorderBrush="White">
<Grid Height="20" Width="30" >
<TextBlock Text="{Binding Name}"/>
</Grid>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
there is a list view and textblock
i need when the selectedItem changed it changed the the background of the selected item
here is the viewmodel
public class MainViewModel : ViewModelBase
{
private Item selectedItem;
public ObservableCollection<Item> List { get; set; }
string text;
public string Text
{
get { return text; }
set
{
text = value;
OnPropertyChanged("Text");
}
}
public Item SelectedItem
{
get { return selectedItem; }
set{
if (value.Name != "Test1")
{
selectedItem = value;
Text = value.Name;
}
else
{
Text = string.Format("Test1 was selected but the selected item is {0}", selectedItem==null?"null":selectedItem.Name);
}
OnPropertyChanged("SelectedItem");
}
}
public MainViewModel()
{
List = new ObservableCollection<Item>()
{
new Item("Test1","Val1"),new Item("Test2","Val2"),new Item("Test3","Val3"),new Item("Test4","Val"),
};
OnPropertyChanged("List");
}
}
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(params string[] propertyNames)
{
if (PropertyChanged != null)
{
foreach (var propertyName in propertyNames)
{
var e = new PropertyChangedEventArgs(propertyName);
PropertyChanged(this, e);
}
}
}
}
public class Item : ViewModelBase
{
public string Name { get; set; }
public string Value { get; set; }
public Item(string name, string val)
{
Name = name;
Value = val;
OnPropertyChanged("Name");
}
}
note that when the Test1 Item selected the selected item didnot changed but in the view Test1 is marked as selected
At the point your MainViewModel.SelectedItem setter is called by the view, the view has already updated its selected item in the list. The binding simply informs the VM of this fact. The fact that you don't set MainViewModel.selectedItem means nothing to the view.
You would think that raising OnPropertyChanged("SelectedItem"); would force the view to re-evaluate its selected item, but in practice this does not work. I assume is down to some optimization within WPF or to prevent cyclic binding updates. (Remember you setter is already being called as part of a binding update, and you are trying to update the binding again)
If you wish to prevent something being selected in the view, then you need to disable it within the view, before it gets down to the VM. Here is one way of doing this.

Validating ItemsControl on Button click in WPF

I have an ItemsControl with an item template that contains two ComboBoxes. For any given item, the second ComboBox is required iff the first ComboBox has a selected value. I have set this validation up using IDataErrorInfo on the view model.
Rather than flagging ComboBox #2 as invalid the second a user selects a value in ComboBox1, I want to perform the validation when the user tries to save. It's kind of annoying to have a form "yell" at you for doing something wrong on a field you haven't even had a chance to enter yet.
Normally you could force this validation by retrieving the BindingExpression for the ComboBox and calling UpdateSource() and then determine if there is an error by calling Validation.GetHasError() passing the ComboBox. Since the ComboBoxes are generated dynamically by the ItemsControl, it is not as easy to get to. So I have 2 questions: 1. How do you ensure validation has executed for all controls when the save button is clicked. 2. How do you check whether there are validation errors when the save button is clicked. Validation.GetHasError remains false for the ItemsControl even when a ComboBox2 within it has an error. Thanks.
EDIT:
I had followed this article to implement IDataErrorInfo in order to validate the combobox properties relative to each other.
public class IntroViewModel : INotifyPropertyChanged, IDataErrorInfo
{
public Guid ClassScheduleID
{
get { return _intro.ClassScheduleID; }
set
{
_intro.ClassScheduleID = value;
OnPropertyChanged("ClassScheduleID");
//OnPropertyChanged("TrialDate"); //This will trigger validation on ComboBox2 when bound ComboBox1 changes
}
}
public DateTime TrialDate
{
get { return _intro.TrialDate; }
set
{
_intro.TrialDate = value;
OnPropertyChanged("TrialDate");
}
}
public string Error
{
get { return null; }
}
public string this[string columnName]
{
get { return ValidateProperty(columnName); }
}
private string ValidateProperty(string propertyName)
{
string error = null;
switch (propertyName)
{
case "TrialDate":
if (_intro.TrialDate == DateTime.MinValue && _intro.ClassScheduleID != Guid.Empty)
error = "Required";
break;
default:
error = null;
break;
}
return error;
}
}
I attempted to create the behavior you need based on some assumptions
sample
XAML
<StackPanel>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding AddItem}"
Content="Add Item" />
<Button Command="{Binding Save}"
Content="Save" />
</StackPanel>
<ItemsControl ItemsSource="{Binding Data}"
Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="border"
BorderThickness="1"
Padding="2"
Margin="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="value1" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ComboBox Text="{Binding Value1}"
ItemsSource="{Binding Source={StaticResource sampleData}}" />
<ComboBox Text="{Binding Value2}"
ItemsSource="{Binding Source={StaticResource sampleData}}"
Grid.Column="1" />
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsValid}"
Value="False">
<Setter TargetName="border"
Property="BorderBrush"
Value="Red" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
Main VM
public ViewModel()
{
AddItem = new SimpleCommand(i => Data.Add(new DataViewModel(new DataModel())));
Save = new SimpleCommand(i =>
{
foreach (var vm in Data)
{
vm.ValidateAndSave();
}
}
);
Data = new ObservableCollection<DataViewModel>();
}
public ObservableCollection<DataViewModel> Data { get; set; }
public ICommand AddItem { get; set; }
public ICommand Save { get; set; }
data VM and model
public class DataModel
{
public object Value1 { get; set; }
public object Value2 { get; set; }
}
public class DataViewModel : INotifyPropertyChanged
{
DataModel model;
public DataViewModel(DataModel model)
{
this.model = model;
IsValid = true;
}
object _value1;
public object Value1
{
get
{
return _value1;
}
set
{
_value1 = value;
}
}
object _value2;
public object Value2
{
get
{
return _value2;
}
set
{
_value2 = value;
}
}
public bool IsValid { get; set; }
public void ValidateAndSave()
{
IsValid = !(_value1 != null && _value2 == null);
PropertyChanged(this, new PropertyChangedEventArgs("IsValid"));
if (IsValid)
{
model.Value1 = _value1;
model.Value2 = _value2;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
so the VM will validate all the items when you click save and will save only those items which are valid. otherwise will mark the IsValid property to false which will be notified to UI
I can't tell how you've implemented the IDataErrorInfo interface in your code, but in my implementation, doing what you want is simple. For future users, you can find out about this interface on the IDataErrorInfo Interface page on MSDN. On the linked page, you will see that you need to implement the Item indexer and the Error property.
That's all you need, because if you have implemented it correctly, then you can find out if your data (implementing) item has an error by simply checking the value of the Error property:
bool hasError = string.IsNullOrEmpty(yourDataTypeInstance.Error);
if (!hasError) Save(yourDataTypeInstance);
else MessageBox.Show("Invalid data!");
UPDATE >>>
Try using this instead:
public DateTime TrialDate
{
get { return _intro.TrialDate; }
set
{
_intro.TrialDate = value;
OnPropertyChanged("TrialDate");
OnPropertyChanged("Error");
}
}
public string Error
{
get { return this["TrialDate"]; }
}
I'll leave you to work out the rest, which is essentially managing strings.
Here is how I accomplished it while waiting for answers. When a save is intiated, ValidateTrials() is called to ensure validation has fired for the comboboxes and then TrialsHaveErrors() is called to check whether there are validation errors on them. This is the brute force approach I'd like to avoid, but it does work.
//Force validation on each combobox2
private void ValidateTrials()
{
foreach (IntroViewModel introVm in icTrials.Items)
{
ContentPresenter cp = (ContentPresenter)icTrials.ItemContainerGenerator.ContainerFromItem(introVm);
if (cp == null) continue;
ComboBox cb2 = (ComboBox)cp.ContentTemplate.FindName("cb2", (FrameworkElement)cp);
//Update the source to force validation.
cb2.GetBindingExpression(ComboBox.SelectedValueProperty).UpdateSource();
}
}
//Recursively searches the Visual Tree for ComboBox elements and checks their errors state
public bool TrialsHaveError(DependencyObject ipElement)
{
if (ipElement!= null)
{
for (int x = 0; x < VisualTreeHelper.GetChildrenCount(ipElement); x++)
{
DependencyObject child = VisualTreeHelper.GetChild(ipElement, x);
if (child != null && child is ComboBox)
{
if (Validation.GetHasError(child))
return true;
}
if (TrialsHaveError(child)) return true; //We found a combobox with an error
}
}
return false;
}
Slimmed down XAML:
<ItemsControl Name="icTrials" ItemsSource="{Binding Intros}" Margin="10,6,10,0" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid Grid.Row="2">
<ComboBox Name="cb1"
SelectedValuePath="ID"
SelectedValue="{Binding Path=ClassScheduleID, Converter={StaticResource nullEmptyConverter}, ConverterParameter=System.Guid}"
ItemsSource="{Binding ClassesSource}">
<ComboBox.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox Name="cb2"
ItemsSource="{Binding AvailableStartDates}"
DisplayMemberPath="Date"
ItemStringFormat="{}{0:d}"
SelectedValue="{Binding Path=TrialDate, Converter={StaticResource nullEmptyConverter}, ConverterParameter=System.DateTime, ValidatesOnDataErrors=True}">
</ComboBox>
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
To avoid the issue of flagging the field invalid before the user has had a chance to set it, I updated the setter for cb1's bound property, ClassScheduleID to conditionally fire notification for the TrialDate property depending on how the value is changing.

Categories