Is the string property immutable in WPF MVVM? - c#

My model:
sealed class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
My ViewModel:
// INotifyPropertyChanged notifies the View of property changes, so that Bindings are updated.
sealed class MyViewModel : INotifyPropertyChanged
{
private User user;
public string FirstName {
get {return user.FirstName;}
set {
if(user.FirstName != value) {
user.FirstName = value;
OnPropertyChange("FirstName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
public string LastName {
get { return user.LastName; }
set {
if (user.LastName != value) {
user.LastName = value;
OnPropertyChange("LastName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
// This property is an example of how model properties can be presented differently to the View.
// In this case, we transform the birth date to the user's age, which is read only.
public int Age {
get {
DateTime today = DateTime.Today;
int age = today.Year - user.BirthDate.Year;
if (user.BirthDate > today.AddYears(-age)) age--;
return age;
}
}
// This property is just for display purposes and is a composition of existing data.
public string FullName {
get { return FirstName + " " + LastName; }
}
public MyViewModel() {
user = new User {
FirstName = "John",
LastName = "Doe",
BirthDate = DateTime.Now.AddYears(-30)
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChange(string propertyName) {
if(PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Then in xaml we use binding:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="4" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold"/>
<Label Grid.Column="0" Grid.Row="1" Margin="4" Content="First Name:" HorizontalAlignment="Right"/>
<!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. -->
<TextBox Grid.Column="1" Grid.Row="1" Margin="4" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="2" Margin="4" Content="Last Name:" HorizontalAlignment="Right"/>
<TextBox Grid.Column="1" Grid.Row="2" Margin="4" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="3" Margin="4" Content="Age:" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="4" Text="{Binding Age}" HorizontalAlignment="Left"/>
</Grid>
The code behind:
public partial class MainWindow : Window
{
private readonly MyViewModel _viewModel;
public MainWindow() {
InitializeComponent();
_viewModel = new MyViewModel();
// The DataContext serves as the starting point of Binding Paths
DataContext = _viewModel;
}
}
My question is that for binding. For example, if FirstName is changed, the view is changed as well. We know string in C# is immutable, when the value changes a new object is created. But in WPF binding, if FirstName is changed, is a new FirstName object created? I assume it is NO since we use binding to hold the value in one object. So does OnPropertyChange remove the immutable internally?

No, FirstName is not reassigned or mutated. It is a reference to a backing string that is reassigned.
When you have an auto-implemented property:
public string FirstName { get; set; }
It is compiled (roughly) into:
private string _firstName;
public string FirstName
{
get => _firstName;
set => _firstName = value;
}
As you can see, assigning a value to FirstName passes the value as an argument to the setter for FirstName which reassigns _firstName (the private backing field). FirstName is never changed.
This is also why INotifyProperyChanged is required. Because the binding is on FirstName and FirstName never changes, the framework needs a way to tell when the value referenced by FirstName changes.

In MVVM, you bind to Properties (not to the backing members).
When you change a Property, like a string, then you technically place a new object behind the class member (i.e. the backing instance of a property).
OnPropertyChanged tells that to the View, so it calls the Property Getter (not the string instance) again, getting a the new instance etc...
If you would not, the View would hold and display a reference to the old string, which is no longer used by your Viewmodel
Then, we have
ObservableCollections and Observable types, that can do this notification automatically, because they are mutable, meaning they stay the same instance, while only their content changes.
Most commonly, ObservableCollections are used, to notify the View, that it contents has changed (while the collection instance stayed the same). (element added, removed..)
In that case, you dont need to Notify, because the View has registered to that event.
If you would assign a new instance to an observableCollection member, the view would also not update correctly without Propertychanged, because the original instance of the collection would no longer change.

Related

WPF, MVVM Populating Cascading ComboBoxes from a dictionary

So, I have a UserControl which contains 2 comboboxes, which I want to be filled from a dictionary. So ComboBoxA gets filled with dictionary keys, and ComboBoxB get filled with the dictionary[ComboBoxA selected item]. How can I achieve that using MVVM? Category is basically int, and Parameter is a string.
My code so far:
Model
public class CategoryUserControlModel
{
public Dictionary<Category, List<Parameter>> parametersOfCategories { get; set;}
public Category chosenCategory { get; set; }
public Parameter chosenParameter { get; set; }
}
ViewModel
public class CategoryUserControlViewModel
{
public CategoryUserControlViewModel(CategoryUserControlModel controlModel)
{
Model = controlModel;
}
public CategoryUserControlModel Model { get; set; }
public Category ChosenCategory
{
get => Model.chosenCategory;
set
{
Model.chosenCategory = value;
}
}
public Parameter ChosenParameter
{
get => Model.chosenParameter;
set => Model.chosenParameter = value;
}
}
XAML
<Grid>
<ComboBox x:Name="Categories" HorizontalAlignment="Left" Margin="0,14,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Model.parametersOfCategories.Keys}"/>
<TextBlock x:Name="Text" HorizontalAlignment="Left" Height="15" Margin="0,-2,0,0" TextWrapping="Wrap" Text="Категория" VerticalAlignment="Top" Width="60"/>
<ComboBox x:Name="Parameter" HorizontalAlignment="Left" Margin="125,14,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Model.parametersOfCategories.Values}/>
<TextBlock x:Name="ParameterText" HorizontalAlignment="Left" Height="15" Margin="125,-2,0,0" TextWrapping="Wrap" Text="Параметр" VerticalAlignment="Top" Width="60"/>
</Grid>
</UserControl>
You are using inappropriate names for your model and viewmodel, they should never be related to a view, and your model should not have members with names that give the impression that the model need user interaction. Consider an improved version similar to this one:
public class CategoryWithParameterModel
{
public Dictionary<Category, List<Parameter>> ParametersOfCategories { get; set;}
public Category Category { get; set; }
public Parameter Parameter { get; set; }
}
Your viewmodel must implement INotifyPropertyChanged interface in order to inform the UI that it need to refresh bindings, this is not necessary for model since you are wrapping it in viewmodel. That is said, your viewmodel definition would become something like:
public class CategoryWithParameterViewModel : INotifyPropertyChanged
{ ... }
Next, since you want to bind to a list from the dictionary then your viewmodel have to expose a property which point to that list, let's call it AvailableParameters, so it should be defined like this:
public List<Parameter> AvailableParameters
{
get
{
if (Model.ParametersOfCategories.ContainsKey(ChosenCategory))
return Model.ParametersOfCategories[ChosenCategory];
else
return null;
}
}
This is the property that need to be bound to ItemsSource of second combobox named "Parameter" :)
However, the property ChosenCategory is not bound at all so you need to bind it to selected item of first combobox to be able to detect user choice which allow the viemodel to find the list of parameters, same thing applies to ChosenParameter, so here is the updated xaml code:
<Grid>
<ComboBox x:Name="Categories" HorizontalAlignment="Left" Margin="0,14,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Model.ParametersOfCategories.Keys}" SelectedItem="{Binding ChosenCategory}"/>
<TextBlock x:Name="Text" HorizontalAlignment="Left" Height="15" Margin="0,-2,0,0" TextWrapping="Wrap" Text="Категория" VerticalAlignment="Top" Width="60"/>
<ComboBox x:Name="Parameter" HorizontalAlignment="Left" Margin="125,14,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding AvailableParameters}" SelectedItem="{Binding ChosenParameter}"/>
<TextBlock x:Name="ParameterText" HorizontalAlignment="Left" Height="15" Margin="125,-2,0,0" TextWrapping="Wrap" Text="Параметр" VerticalAlignment="Top" Width="60"/>
</Grid>
Lastly, you have to notify UI when ChosenCategory has changed so for this you will need to raise PropertyChanged event for AvailableParameters. Implementing that will make the viewmodel become something like this:
public class CategoryWithParameterViewModel : INotifyPropertyChanged
{
public CategoryWithParameterViewModel(CategoryWithParameterModel model)
{
Model = model;
}
// This should be read-only
public CategoryWithParameterModel Model { get; /*set;*/ }
public Category ChosenCategory
{
get => Model.Category;
set
{
Model.Category = value;
OnPropertyChanged(nameof(AvailableParameters));
}
}
public Parameter ChosenParameter
{
get => Model.Parameter;
set => Model.Parameter = value;
}
public List<Parameter> AvailableParameters
{
get
{
if (Model.ParametersOfCategories.ContainsKey(ChosenCategory))
return Model.ParametersOfCategories[ChosenCategory];
else
return null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

C# WPF Nested List in MVVM

I'd like to use a nested ObservableCollection in MVVM in order to add as many groups and users as possible. However, I don't know how to create/add a new user. I also don't know how to bind the new users to the XAML.
(NOTE: this time, I just need two persons.)
I'm new to C#, WPF and MVVM.
I'm learning MVVM referring to this site: https://riptutorial.com/wpf/example/992/basic-mvvm-example-using-wpf-and-csharp
I've been trying this since last week, but no luck.
I tried:
outerObservableCollection.Add(
new ObservableCollection<User>
{
{
FirstName = "Jane",
LastName = "June",
BirthDate = DateTime.Now.AddYears(-20)
}
}
);
... which ends up with the following error:
The name 'BirthDate' does not exist in the current context
(I guess the cause of this issue is that I didn't create a 'user' object, so 'user.BirthDate' is not accessible.)
Let me show the entire codes.
MainWindow.xaml:
<Window x:Class="MVVM_RIP_Tutorial.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MVVM_RIP_Tutorial"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="32.8"/>
<RowDefinition Height="28.8"/>
<RowDefinition Height="38*"/>
<RowDefinition Height="37*"/>
<RowDefinition Height="38*"/>
<RowDefinition Height="155*"/>
</Grid.RowDefinitions>
<!--1st Person-->
<TextBlock Grid.Column="1" Grid.Row="0" Margin="320.6,4,398.6,3.2" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold" Width="0"/>
<Label Grid.Column="0" Grid.Row="1" Margin="0,4.8,4.4,2.8" Content="First Name:" HorizontalAlignment="Right" Width="70"/>
<!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. -->
<TextBox Grid.Column="1" Grid.Row="1" Margin="3.6,4.8,0,1.8" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="2" Margin="0,5.2,4.4,1.6" Content="Last Name:" HorizontalAlignment="Right" Width="69"/>
<TextBox Grid.Column="1" Grid.Row="2" Margin="3.6,5.2,0,1.6" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="3" Margin="0,6.4,4.4,4.2" Content="Age:" HorizontalAlignment="Right" Grid.RowSpan="2" Width="33"/>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="3.6,6.4,0,10.2" Text="{Binding Age}" HorizontalAlignment="Left" Grid.RowSpan="2" Width="0"/>
<!--2nd Person-->
<!--<TextBlock Grid.Column="1" Grid.Row="4" Margin="320.6,4,398.6,3.2" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold" Width="0"/>
<Label Grid.Column="0" Grid.Row="5" Margin="0,4.8,4.4,2.8" Content="First Name:" HorizontalAlignment="Right" Width="70"/>
--><!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. --><!--
<TextBox Grid.Column="1" Grid.Row="5" Margin="3.6,4.8,0,1.8" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="6" Margin="0,5.2,4.4,1.6" Content="Last Name:" HorizontalAlignment="Right" Width="69"/>
<TextBox Grid.Column="1" Grid.Row="6" Margin="3.6,5.2,0,1.6" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="7" Margin="0,6.4,4.4,4.2" Content="Age:" HorizontalAlignment="Right" Grid.RowSpan="2" Width="33"/>
<TextBlock Grid.Column="1" Grid.Row="7" Margin="3.6,6.4,0,10.2" Text="{Binding Age}" HorizontalAlignment="Left" Grid.RowSpan="2" Width="0"/>-->
</Grid>
</Window>
MainWindow.xaml.cs:
using System.Windows;
namespace MVVM_RIP_Tutorial
{
public partial class MainWindow : Window
{
private readonly MyViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MyViewModel();
// The DataContext serves as the starting point of Binding Paths
DataContext = _viewModel;
}
}
}
User.cs:
using System;
namespace MVVM_RIP_Tutorial
{
sealed class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
}
MyViewModel.cs:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace MVVM_RIP_Tutorial
{
// INotifyPropertyChanged notifies the View of property changes, so that Bindings are updated.
sealed class MyViewModel : INotifyPropertyChanged
{
private User user;
ObservableCollection<ObservableCollection<User>> outerObservableCollection
= new ObservableCollection<ObservableCollection<User>>();
//ObservableCollection<User> user = new ObservableCollection<User>();
public string FirstName
{
get { return user.FirstName; }
set
{
if (user.FirstName != value)
{
user.FirstName = value;
OnPropertyChange("FirstName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
public string LastName
{
get { return user.LastName; }
set
{
if (user.LastName != value)
{
user.LastName = value;
OnPropertyChange("LastName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
// This property is an example of how model properties can be presented differently to the View.
// In this case, we transform the birth date to the user's age, which is read only.
public int Age
{
get
{
DateTime today = DateTime.Today;
int age = today.Year - user.BirthDate.Year;
if (user.BirthDate > today.AddYears(-age)) age--;
return age;
}
}
// This property is just for display purposes and is a composition of existing data.
public string FullName
{
get { return FirstName + " " + LastName; }
}
public MyViewModel()
{
user = new User
{
FirstName = "John",
LastName = "Doe",
BirthDate = DateTime.Now.AddYears(-30)
};
//outerObservableCollection.Add(user);
//outerObservableCollection.Add(
// new ObservableCollection<User>
// {
// {
// FirstName = "Jane",
// LastName = "June",
// BirthDate = DateTime.Now.AddYears(-20)
// }
// }
//);
);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChange(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
... Please help me. Thank you in advance.
First of all, welcome to C#, WPF, and MVVM!
From your description it sounds like you would like to display a tree of users within groups... with that in mind, you could implement something like this to accomplish that goal:
Models
public class GroupModel
{
public GroupModel(uint id, string name)
{
Id = id;
Name = name;
}
public uint Id { get; }
public string Name { get; }
}
public class UserModel
{
public UserModel(uint id, string firstName, string surname, DateTime dateOfBirth)
{
Id = id;
FirstName = firstName;
Surname = surname;
DateOfBirth = dateOfBirth;
}
public uint Id { get; }
public string FirstName { get; }
public string Surname { get; }
public DateTime DateOfBirth { get; }
}
ViewModels
Base classes
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public abstract class ViewModelBase<TModel> : ViewModelBase
where TModel : class
{
private TModel _model;
public ViewModelBase(TModel model)
=> _model = model;
/*
* There is a design choice here to allow the model
* to be swapped at runtime instead or to treat the
* view model as immutable in which case the setter
* for the Model property should be removed.
*/
public TModel Model
{
get => _model;
set
{
if (ReferenceEquals(_model, value))
{
return;
}
_model = value;
OnPropertyChanged();
}
}
}
Concrete classes
public class GroupViewModel : ViewModelBase<GroupModel>
{
public GroupViewModel(GroupModel model)
: base(model)
{
}
public ObservableCollection<UserViewModel> Users { get; }
= new ObservableCollection<UserViewModel>();
public void AddUser(UserModel user)
{
var viewModel = new UserViewModel(user);
Users.Add(viewModel);
}
}
public class UserViewModel : ViewModelBase<UserModel>
{
public UserViewModel(UserModel model)
: base(model)
{
}
// convenience property; could be done completely in XAML as well
public string FullName => $"{Model.FirstName} {Model.Surname}";
}
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
// create sample user groups
for (var groupIndex = 1u; groupIndex <= 5u; ++groupIndex)
{
var groupName = $"Group {groupIndex}";
var groupModel = new GroupModel(groupIndex, groupName);
var groupViewModel = new GroupViewModel(groupModel);
UserGroups.Add(groupViewModel);
for (var userIndex = 1u; userIndex <= 5u; ++userIndex)
{
var userModel = new UserModel(
id: userIndex,
firstName: "John",
surname: $"Smith",
dateOfBirth: DateTime.Today);
groupViewModel.AddUser(userModel);
}
}
}
public ObservableCollection<GroupViewModel> UserGroups { get; }
= new ObservableCollection<GroupViewModel>();
}
View
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:UserGroups.ViewModels"
x:Class="UserGroups.Views.MainWindow"
Title="User Groups"
Width="1024"
Height="768">
<Window.DataContext>
<viewModels:MainViewModel />
</Window.DataContext>
<Grid>
<TreeView ItemsSource="{Binding Path=UserGroups}">
<TreeView.Resources>
<!-- Template for Groups -->
<HierarchicalDataTemplate DataType="{x:Type viewModels:GroupViewModel}"
ItemsSource="{Binding Path=Users}">
<TextBlock Text="{Binding Path=Model.Name}" />
</HierarchicalDataTemplate>
<!-- Template for Users -->
<DataTemplate DataType="{x:Type viewModels:UserViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Model.Id, StringFormat='[{0}]'}"
Margin="3,0" />
<TextBlock Text="{Binding Path=FullName}"
Margin="3,0" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
Here's what you end up with:
There are lots of frameworks that take care of a lot of the tedious work of working with the MVVM pattern (e.g. removing most/all of the boilerplate code for INotifyPropertyChanged). Here are just a few to look at:
MVVM Light Toolkit
Prism
ReactiveUI
Some additional resources that might also be useful:
SnoopWPF
WPF Tutorial
It's not entirely clear to me what the result is supposed to look like, but here are some initial suggestions.
I wouldn't try nesting an Observable collection inside another one. Instead, define something like a separate Group model class that has a list of User objects as its field.
I take it the user is supposed to enter values for your bound properties in the xaml in order to create a new user? You need to add a button or something that the user can press after filling those values out. The button click should be bound to a RelayCommand (add MVVMLight to the project if necessary) in the view model. The method invoked by said relaycommand would instantiate a new User object using the fields bound in the xaml and add to your ObservableCollection.
<Button Command="{Binding Path=CreateUserCommand}">
<TextBlock Text="Create User"/>
</Button>
Then in the view model...
public RelayCommand CreateUserCommand { get; private set; }
CreateUserCommand = new RelayCommand(() =>
{
User user = new User
{
FirstName = FirstName,
LastName = LastName,
//...etc.
}
collectionOfUsers.Add(user);
});
"I also don't know how to bind the new users to the XAML."
So far I don't see any xaml code that would handle displaying new users. Seems to me you'd want to bind your collection of users to a grid or combo box. After the user enters new user properties in the textboxes and clicks the appropriate button, the grid or combo box would update. You could have separate controls for separate groups. Again, that part is not entirely clear to me.
Given (3), your ObservableCollection of users needs to be a property in the view model that implements OnPropertyChanged.
Hope that helps.

C# Wpf Binding to Collection of Userdefined Class not working as expected

Currently i have an ObservableCollection of MyClass in my Viewmodel. I use the getter of this Collection to fill it from an other Datasource. I can now Display this Data in a Window(Grid) and the correct Data is shown, but when i change the Data the set is not fired(I think it is because not the Collection is changed, only a Element in the Collection). Should i create a Property for every Property of MyClass, so i can react to the changes of a single Value, the Questions i ask myself are:
How do i know what Element is selected at the moment
How to fill the Collection correct when i have a binding to every single item
I also thought of a Event when my Collection is changed, but i am not sure how to implement it right.
public ObservableCollection<MyClass<string>> SelectedParams
{
get
{
//Fill the Collection
}
set
{
//I think here i want to react to changed Textboxvalues in my View
}
}
public class MyClass<T> : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private T _curValue;
private string _value1;
private string _value2;
public string Value1
{
get
{
return _value1;
}
set
{
if (_value1 != value)
{
_value1 = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value1)));
}
}
}
public string Value2
{
get
{
return _value2;
}
set
{
if (_value2 != value)
{
_value2 = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value2)));
}
}
}
public T curValue
{
get
{
return _curValue;
}
set
{
_curValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(curValue)));
}
}
public MyClass()
{
}
public MyClass(string val1, string val2, T curVal)
{
Value1 = val1;
Value2 = val2;
curValue = curVal;
}
}
The xaml Code looks something like this
<ItemsControl ItemsSource="{Binding SelectedParams}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding Value1}"/>
<Label Grid.Column="1" Content="{Binding Value2}"/>
<TextBox Grid.Column="2" Text="{Binding curValue, Mode=TwoWay}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Edit1: Changed MyClass to INotifyPropertyChanged now the Collection changes internal Values but the Setter is still not called on change of a Value
You need to implement INotifyPropertChanged interface for MyClass and raise the PropertyChanged in setter to notify UI that the property value changed.
How do i know what Element is selected at the moment
If you want support for item selection you have to use an other control. ItemsControl does not support selection.
Use ListView for example. Bind ItemsSource and SelectedItem to your class. Now every time you click on an item, SelectedValue is updated. And if you change SelectedValue from code the UI updates the selected item in the list. You can also bind other controls to SelectedValue like I did with the TextBlock outside the ListView.
View
<StackPanel>
<ListView ItemsSource="{Binding Values}" SelectedItem="{Binding SelectedValue}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Item1}" />
<TextBlock Text="=" />
<TextBlock Text="{Binding Path=Item2}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel Orientation="Horizontal">
<TextBox Text="Selected:" Background="DarkGray" />
<TextBox Text="{Binding SelectedValue.Item1, Mode=OneWay}" Background="DarkGray" />
</StackPanel>
</StackPanel>
Data
public class ListViewBindingViewModel : INotifyPropertyChanged
{
private Tuple<string,int> _selectedValue;
public ObservableCollection<Tuple<string,int>> Values { get; }
public Tuple<string, int> SelectedValue
{
get { return _selectedValue; }
set
{
_selectedValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedValue)));
}
}
public ListViewBindingViewModel()
{
Values = new ObservableCollection<Tuple<string, int>> {Tuple.Create("Dog", 3), Tuple.Create("Cat", 5), Tuple.Create("Rat",1)};
}
}

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.)

mvvm windows phone 8 confusion and how to capture when text is changed

I'm trying to write a windows phone 8 app using the mvvm pattern but I'm struggling with it.
I have a page with a list of persons which is binded to my PersonViewModel. That part is working fine. I then have 2 buttons in the application bar i.e. add or edit. When I want to edit a person, I select the person from the list, which then sets the CurrentPerson in my ViewModel. This in turns set a property in my MainViewModel which is used to store the currently selected person i.e.
App.MainViewModel.CurrentPerson = this.CurrentPerson;
When I want to add a new person, I use the same principal but I create a new person model.
App.MainViewModel.CurrentPerson = new PersonModel();
I then redirect to a page which contains the fields to handle a person, whether it is being added or edited and this is binded to a ViewModel called PersonEntryViewModel
Before I explain my problem, I want to let you know what I'm trying to achieve. I want the "Save" button in my application bar to get enabled once a certain amount of criteria have been met i.e. Name has been filled and has x characters, etc...
I can see what my problem is but I don't know how to resolve it.
Here is a simplied version of my PersonEntryViewModel:
public class PersonEntryViewModel : BaseViewModel
{
private PersonModel _currentPerson;
private bool _isNewPerson;
private ICommand _savePersonCommand;
private ICommand _cancelCommand;
private ICommand _titleTextChanged;
private bool _enableSaveButton;
public PersonEntryViewModel()
{
this.CurrentPerson = App.MainViewModel.CurrentPerson ?? new PersonModel();
}
public ICommand SavePersonCommand
{
get
{
return this._savePersonCommand ?? (this._savePersonCommand = new DelegateCommand(SavePersonAction));
}
}
public ICommand CancelCommand
{
get
{
return this._cancelCommand ?? (this._cancelCommand = new DelegateCommand(CancelAction));
}
}
public ICommand NameTextChanged
{
get
{
return this._nameTextChanged ?? (this._nameTextChanged = new DelegateCommand(NameTextChangedAction));
}
}
private void NameTextChangedAction(object actionParameters)
{
if (!string.IsNullOrEmpty(this._currentPerson.Name) && _currentPerson.Name.Length > 2)
{
EnableSaveButton = true;
}
}
private void CancelAction(object actionParameters)
{
Console.WriteLine("Cancel");
INavigationService navigationService = this.GetService<INavigationService>();
if (navigationService == null)
return;
navigationService.GoBack();
navigationService = null;
}
private void SavePersonAction(object actionParameters)
{
Console.WriteLine("Saving");
}
public PersonModel CurrentPerson
{
get { return this._currentPerson; }
set
{
if (this._currentPerson != value)
this.SetProperty(ref this._currentPerson, value);
}
}
public string PageTitle
{
get { return this._pageTitle; }
set { if (this._pageTitle != value) this.SetProperty(ref this._pageTitle, value); }
}
public bool IsNewPerson
{
get { return this._isNewPerson; }
set
{
if (this._isNewPerson != value)
{
this.SetProperty(ref this._isNewPerson, value);
if (this._isNewPerson)
this.PageTitle = AppResources.PersonEntryPageNewTitle;
else
this.PageTitle = AppResources.PersonEntryPageEditTitle;
}
}
}
public bool EnableSaveButton
{
get { return this._enableSaveButton; }
set { if (this._enableSaveButton != value) this.SetProperty(ref this._enableSaveButton, value); }
}
}
Here is part of my XAML:
<Grid x:Name="LayoutRoot" Background="Transparent" DataContext="{StaticResource PersonEntryViewModel}" >
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" DataContext="{Binding CurrentPerson, Mode=TwoWay}">
<Grid Margin="0,0,0,5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border BorderBrush="{StaticResource PhoneAccentBrush}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderThickness="5"
Background="Transparent"
CornerRadius="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Text="Name:"
Grid.Row="0"
Margin="12,0,0,0"/>
<TextBox Text="{Binding Name, Mode=TwoWay}"
Grid.Row="1">
<!--<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding NameTextChanged, Mode=OneWay}" CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>-->
</TextBox>
<TextBlock Text="Address:"
Grid.Row="2"
Margin="12,0,0,0"/>
<TextBox Text="{Binding Address, Mode=TwoWay}"
AcceptsReturn="True"
Height="200"
VerticalScrollBarVisibility="Visible"
TextWrapping="Wrap"
Grid.Row="3"/>
As you can see, my layoutRoot grid is binded to my ViewModel i.e. PersonEntryViewModel and the grid content panel containing my textboxes required for editing is binded to CurrentPerson.
Is that the correct way to do it? I need to bind the control to the CurrentPerson property which will contain data if the person is being edited and it will contain a new empty PersonModel if a new person is being added.
As it stands, that part is working. When I type some text in my field and click on the next one, it calls set the CurrentPerson relevant property which in turns calls the PersonModel. Click on the save button and I check the CurrentPerson, I can see it has all the various properties set.
As you can see in my PersonEntryViewModel, I've got other properties which are required. For example the EnableSaveButton, which technically should be set to true or false based on the validation of the various properites from the CurrentPerson object but I need this to be checked as the user is typing text in the various textbox and this is where I'm having a problem.
If I enable the following code:
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding NameTextChanged, Mode=OneWay}" CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>
It doesn't get triggered in the PersonEntryViewModel where I really need it as this is where I want to set my EnableSaveButton property but I guess it makes sense as this code is binded to the Name textbox which is turn is binded to the CurrentPerson property which is my PersonModel.
If I move the code from the PersonEntryViewModel to the PersonViewModel
private ICommand _personTextChanged;
public ICommand PersonTextChanged
{
get
{
return this._personTextChanged ?? (this._personTextChanged = new DelegateCommand(PersonTextChangedAction));
}
}
private void PersonTextChangedAction(object actionParameters)
{
if (!string.IsNullOrEmpty(this._name) && this._name.Length > 2)
{
//EnableSaveButton = true;
Console.WriteLine("");
}
}
It gets triggered accordingly but then how do I get this information back to my PersonEntryViewModel which binded to the view where my 2 buttons (i.e. save & cancel) are located and the EnableSaveButton property is responsible for enabling the save button accordingly when set assuming that the Name is valid i.e. set and minlen is match for example.
Is the PersonEntryViewModel and using a CurrentPerson property with the current person being edited or added designed correctly or not and how am I to handle this scenario?
I hope the above makes sense but if I'm not clear about something, let me know and I'll try to clarify it.
Thanks.
PS: I posted another posted related to how to detect text change, but I figured it out but it's obviously not the problem. The problem seems more related to design.
Your design is unclear to me.
If you want to go with the current design itself, I would suggest you do the following thing.
Remove assigning the DataContext for grid in xaml.
In the code behind add:
var dataContext = new PersonEntryViewModel();
this.ContentPanel.DataContext = dataContext.CurrentPerson;
// After creating App bar
this.appBar.DataContext = dataContext;
//Your xaml code will look something like this:
<AppBar x:Name="appBar">
<Button x:Name="saveBtn" IsEnabled={Binding EnableSaveButton} />
<AppBar />

Categories