How to bind to child view model property in WPF application? - c#

I am developing WPF application with Prism MVVM framework. And I doesn't know how properly pass data between parent and child view models.
I have 2 view models - ParentViewModel and inner ChildViewModel.
public class ParentViewModel
{
public ParentViewModel
{
ChildViewModel = new ChildViewModel(params);
}
private ChildViewModel _childViewModel;
public ChildViewModel ChildViewModel
{
get { return _childViewModel; }
set
{
SetProperty(ref _childViewModel, value);
}
}
//This is does not work
public int SelectedChildNumber
{
return _childViewModel.SelectedNumber;
}
}
public class ChildViewModel
{
public ChildViewModel
{
_numbers = new List<int>();
}
private List<int> _numbers;
public List<int> Numbers
{
get { return _numbers; }
set
{
SetProperty(ref _numbers, value);
}
}
private int _selectedNumber;
public int SelectedNumber
{
get { return _selectedNumber; }
set
{
SetProperty(ref _selectedNumber, value);
}
}
}
I want to get and use selected value from child view model. My approach doesn't work - SelectedChildNumber doesn't want to refresh if SelectedNumber changes in ChildViewModel.
UPDATE:
Ok, What if I have ChildViewModel collection in ParentViewModel. One of this ChildViewModels have property IsSelected equals true. How to get this one selected view model from collection?
public class ParentViewModel
{
public ParentViewModel
{
Items = GetItems();
}
private ObservableCollection<ChildViewModel> _items;
public ObservableCollection<ChildViewModel> Items
{
get
{
return _items;
}
set
{
SetProperty(ref _items, value);
}
}
}
public class ChildViewModel
{
public ChildViewModel
{
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
SetProperty(ref _isSelected, value);
}
}
}
How to get selected view model? Maybe use a converter?
<someUserControl DataContext="{Binding ParentViewModel.Items, Converter={x:Static c:GetSelectedItemConverter.Instance}}" />
In converter I can find selected item. Or this is bad idea?
UPDATE 2:
Ok, I beat this problem with Ed Plunkett help. Final version should be:
public class ParentViewModel
{
public ParentViewModel
{
Items = GetItems();
foreach (var item in Items)
{
item.PropertyChanged += ChildViewModel_PropertyChanged;
}
}
private ObservableCollection<ChildViewModel> _items;
public ObservableCollection<ChildViewModel> Items
{
get
{
return _items;
}
set
{
SetProperty(ref _items, value);
}
}
private ChildViewModel _selectedChild;
public ChildViewModel SelectedChild
{
get { return _selectedChild; }
set
{
SetProperty(ref _selectedChild, value);
}
}
private void ChildViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var child = (ChildViewModel)sender;
if (e.PropertyName == nameof(ChildViewModel.IsSelected) && child.IsSelected)
{
SelectedChild = child;
}
}
}

Bind directly to the child property:
<ListBox
ItemsSource="{Binding ChildViewModel.Numbers}"
SelectedItem="{Binding ChildViewModel.SelectedNumber}"
/>
<Label Content="{Binding ChildViewModel.SelectedNumber}" />
That's the name of the parent's ChildViewModel property in the binding path, not the type. The Binding now knows to listen to the ChildViewModel object for PropertyChanged notifications regarding SelectedNumber and Numbers.
The reason your version doesn't work is that the parent does not raise PropertyChanged when SelectedChildNumber changes. In fact, the parent doesn't know when it changes any more than the UI does. The parent could handle the child's PropertyChanged event, and sometimes that's done.
public ParentViewModel()
{
ChildViewModel = new ChildViewModel(params);
// Handle child's PropertyChanged event
ChildViewModel.PropertyChanged += ChildViewModel_PropertyChanged;
}
private void ChildViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var child = (ChildViewModel)sender;
if (e.PropertyName == nameof(ChildViewModel.SelectedNumber))
{
// Do stuff
}
}
But you don't need to do that for cases like this one.
ChildViewModel.Numbers should probably be ObservableCollection<int>, not List<int>. That way, if you add more numbers to it or remove any, the UI will automatically be notified by the collection and the ListBox will automatically update itself accordingly.

Related

How to handle changes of submodels in main model (MVVM)

What is the best practice to update dynamically created checkboxes states from model? Acutal values for checkboxes are held in submodels of main model and being changed accordingly to it's logic. Checkboxes' properties bind to their individual FooViewModels. But how to change FooViewModel's properties then?
1 way: main model fires especial event -> main VM handles it and finds target FooViewModel to update using event args -> main VM sets target FooViewModel properties with values specified in event args -> checkbox is updated via bindings to FooViewModel
2 way: Main model holds observable collection of FooModels implementing INPC and each is being wrapped with FooViewModel (using CollectionChanged event in main VM). Main model set some FooModel's property -> FooViewModel handles PropertyChanged and transfers it further firing own PropertyChanged event -> checkbox is updated via bindings to FooViewModel.
Transferrence code in FooViewModel:
this._model.PropertyChanged += (s, a) => this.RaisePropertyChangedEvent(a.PropertyName);
My implementation of 2nd way is next:
// MainModel class that holds collection of extra models (CfgActionModel):
class MainModel: BindableBase
{
ObservableCollection<CfgActionModel> _actionsColl
= new ObservableCollection<CfgActionModel>();
public ObservableCollection<CfgActionModel> ActionCollection
{
get => this._actionsColl;
}
public void AddAction(ConfigEntry cfgEntry, bool isMeta)
{
CfgActionModel actionModel = new CfgActionModel()
{
CfgEntry = cfgEntry,
Content = cfgEntry.ToString(),
IsEnabled = true,
IsChecked = false
};
this._actionsColl.Add(actionModel);
}
}
// Extra model that is wrapped with CfgActionViewModel:
class CfgActionModel: BindableBase
{
ConfigEntry _cfgEntry; // Custom enumeration value unique for each checkbox
string _content;
bool _isEnabled = false;
bool _isChecked = false;
public ConfigEntry CfgEntry
{
get => this._cfgEntry;
set
{
if (this._cfgEntry == value) return;
this._cfgEntry = value;
this.RaisePropertyChangedEvent(nameof(CfgEntry));
}
}
public string Content
{
get => this._content;
set
{
if (this._content == value) return;
this._content = value;
this.RaisePropertyChangedEvent(nameof(Content));
}
}
public bool IsEnabled
{
get => this._isEnabled;
set
{
if (this._isEnabled == value) return;
this._isEnabled = value;
this.RaisePropertyChangedEvent(nameof(IsEnabled));
}
}
public bool IsChecked
{
get => this._isChecked;
set
{
if (this._isChecked == value) return;
this._isChecked = value;
this.RaisePropertyChangedEvent(nameof(IsChecked));
}
}
}
// CfgActionViewModel that is checkbox in UI is bound to:
class CfgActionViewModel: BindableBase
{
CfgActionModel _model;
public CfgActionViewModel(CfgActionModel model)
{
this._model = model;
this._model.PropertyChanged += (s, a) => this.RaisePropertyChangedEvent(a.PropertyName);
}
public string Content
{
get => this._model.Content;
set => this._model.Content = value;
}
public bool IsEnabled
{
get => this._model.IsEnabled;
set => this._model.IsEnabled = value;
}
public bool IsChecked
{
get => this._model.IsChecked;
set => this._model.IsChecked = value;
}
}
// MainViewModel where we fill the model with data:
class MainViewModel
{
MainModel model;
readonly ObservableCollection<CfgActionViewModel> _actionVMColl = new ObservableCollection<CfgActionViewModel>();
public ObservableCollection<CfgActionViewModel> ActionVMCollection => this._actionVMColl;
public MainViewModel()
{
this.model = new MainModel();
this.model.ActionCollection.CollectionChanged += (s, a) =>
{
// when new model is created we create new ViewModel wrapping it
if (a.Action == NotifyCollectionChangedAction.Add)
{
CfgActionModel newModel = (CfgActionModel) a.NewItems[0];
CfgActionViewModel actionViewModel = new CfgActionViewModel(newModel);
_actionVMColl.Add(actionViewModel);
}
};
model.AddAction(ConfigEntry.AutoBuy, false);
model.AddAction(ConfigEntry.Bomb, false);
}
}
DataTemplate in View looks like this:
<DataTemplate DataType="{x:Type mvvm:CfgActionViewModel}">
<CheckBox
IsChecked="{Binding Path=IsChecked, Mode=TwoWay}"
IsEnabled="{Binding Path=IsEnabled, Mode=TwoWay}"
Content="{Binding Path=Content, Mode=OneWay}"/>
</DataTemplate>
Is it acceptable by MVVM to avoid interaction with MainViewModel somewhere (2nd way) or each subViewModel's property must be set by MainViewModel (1st way)?
Both approaches are acceptable. But personally, I would do approach #1 to keep my Models as thin as possible.
You can refer to the sample code on how you can do approach #1.
public class MainViewModel : BindableBase
{
public ObservableCollection<SubViewModel> SubViewModels { get; }
public MainViewModel()
{
SubViewModels = new ObservableCollection<SubViewModel>();
SubViewModels.CollectionChanged += SubViewModels_CollectionChanged;
}
private void SubViewModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e.Action == NotifyCollectionChangedAction.Add)
{
foreach(var subVM in e.NewItems.Cast<SubViewModel>())
{
subVM.PropertyChanged += SubViewModel_PropertyChanged;
}
}
// TODO: Unsubscribe to SubViewModels that are removed in collection to avoid memory leak.
}
private void SubViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(SubViewModel.IsChecked):
// TODO: Do your thing here...
break;
}
}
}
public class SubViewModel : BindableBase
{
private bool _isChecked;
public bool IsChecked
{
get => _isChecked;
set => SetProperty(ref _isChecked, value);
}
}
As you can see, I don't even need to include any Models in the sample code which means that all the logic here are all clearly part of the presentation layer.
Now, you can focus on your business/domain logic in your Models.

Dual binding on Checkbox

I have a WPF application using MVVM. I have the IsChecked value bound to a boolean on my model instance on my ViewModel. I also need to bind a method on the ViewModel to the Checked and Unchecked events. (This is so I can track unsaved changes and change the background to give my users visual indication of the need to save. I tried:
<CheckBox
Content="Enable"
Margin="5"
IsChecked="{Binding Enabled}"
Checked="{Binding ScheduleChanged}"
Unchecked="{Binding ScheduleChanged}"
/>
But I get a 'Provide value on 'System.Windows.Data.Binding' threw an exception.' error. Advice?
Here is the Model I am working with:
public class Schedule : IEquatable<Schedule>
{
private DateTime _scheduledStart;
private DateTime _scheduledEnd;
private bool _enabled;
private string _url;
public DateTime ScheduledStart
{
get { return _scheduledStart; }
set
{
_scheduledStart = value;
}
}
public DateTime ScheduledEnd
{
get { return _scheduledEnd; }
set
{
if(value < ScheduledStart)
{
throw new ArgumentException("Scheduled End cannot be earlier than Scheduled Start.");
}
else
{
_scheduledEnd = value;
}
}
}
public bool Enabled
{
get { return _enabled; }
set { _enabled = value; }
}
public string Url
{
get { return _url; }
set { _url = value; }
}
public bool Equals(Schedule other)
{
if(this.ScheduledStart == other.ScheduledStart && this.ScheduledEnd == other.ScheduledEnd
&& this.Enabled == other.Enabled && this.Url == other.Url)
{
return true;
}
else
{
return false;
}
}
}
My viewModel contains a property that has an ObservableCollection. An ItemsControl binds to the collection and generates a list. So my ViewModel sort of knows about my Model instance, but wouldn't know which one, I don't think.
Checked and Unchecked are events, so you can not bind to them like you can IsChecked, which is a property. On a higher level it is also probably wise for your view model not to know about a checkbox on the view.
I would create an event on the view model that fires when Enabled is changed, and you can subscribe to that and handle it any way you like.
private bool _enabled;
public bool Enabled
{
get
{
return _enabled;
}
set
{
if (_enabled != value)
{
_enabled = value;
RaisePropertyChanged("Enabled");
if (EnabledChanged != null)
{
EnabledChanged(this, EventArgs.Empty);
}
}
}
}
public event EventHandler EnabledChanged;
// constructor
public ViewModel()
{
this.EnabledChanged += This_EnabledChanged;
}
private This_EnabledChanged(object sender, EventArgs e)
{
// do stuff here
}
You should be able to just handle this in the setter for Enabled...
public class MyViewModel : ViewModelBase
{
private bool _isDirty;
private bool _enabled;
public MyViewModel()
{
SaveCommand = new RelayCommand(Save, CanSave);
}
public ICommand SaveCommand { get; }
private void Save()
{
//TODO: Add your saving logic
}
private bool CanSave()
{
return IsDirty;
}
public bool IsDirty
{
get { return _isDirty; }
private set
{
if (_isDirty != value)
{
RaisePropertyChanged();
}
}
}
public bool Enabled
{
get { return _enabled; }
set
{
if (_enabled != value)
{
_enabled = value;
IsDirty = true;
}
//Whatever code you need to raise the INotifyPropertyChanged.PropertyChanged event
RaisePropertyChanged();
}
}
}
You're getting a binding error because you can't bind a control event directly to a method call.
Edit: Added a more complete example.
The example uses the MVVM Lite framework, but the approach should work with any MVVM implementation.

How to update a value in parent ViewModel if value in one of it's child is updated in windows 8.1 app

I have a Windows 8.1 application with a parent and child viewmodel in the following relationship
ParentViewModel
class ParentViewModel {
private double _parentAmount;
public double parentAmount
{
get { return _parentAmount; }
set
{
if (value != _parentAmount)
{
_parentAmount = value;
NotifyPropertyChanged("parentAmount");
}
}
}
private ObservableCollection<ChildViewModel> _children;
public ObservableCollection<ChildViewModel> children
{
get { return _children; }
set
{
if (value != _children)
{
_children = value;
NotifyPropertyChanged("children");
}
}
}
}
ChildViewModel
class ChildViewModel {
private double _ChildAmount;
public double ChildAmount
{
get { return _ChildAmount; }
set
{
if (value != _ChildAmount)
{
_ChildAmount = value;
NotifyPropertyChanged("ChildAmount");
}
}
}
}
In the XAML there is TextBlock that is bound to the "ParentAmount" and then there is a ListView bound to the Observable collection "Children". ListView's Itemtemplate is a datatemplate with a TextBox with a two way bind to the "ChildAmount". The user can modify the value in the child TextBox
Now my requriement is to update the ParentAmount with the sum of all its child amount on the fly when the user modifies one of the child amounts. How do I achieve this?
For illustration purpose I have simplified the code example pasted above, the ChildViewModel has more functionality than what can be seen hence I can't replace that ObservableCollection of ChildViewModel with a List of double for instance.
I would be very glad if someone can point me in the right direction. Thanks in Advance.
With a very small addition, this will do the trick.
The specific changes are adding a property change handler for each child object in the ObservableCollection.
Note that this is a crude example to set you on the right track - I haven't unhooked the event handlers, and I recalculate the parent amount on any change from the child (i.e. I don't check that it was the ChildAmount that changed, this means you end up with more action than is necessary). I also haven't put in any code to handle changes to the contents of the ObservableCollection so if new items are added to it they won't have a property change event handler attached - this is simple for you to do yourself.
Note my use of a BaseViewModel - this is just good practice, it saves you from reimplementing the INotifyPropertyChanged interface on every class that needs it.
class ParentViewModel : BaseViewModel
{
private double _parentAmount;
public double parentAmount
{
get { return _parentAmount; }
set
{
if (value != _parentAmount)
{
_parentAmount = value;
NotifyPropertyChanged("parentAmount");
}
}
}
private ObservableCollection<ChildViewModel> _children;
public ObservableCollection<ChildViewModel> children
{
get { return _children; }
set
{
if (value != _children)
{
_children = value;
foreach (var child in _children)
child.PropertyChanged += ChildOnPropertyChanged;
NotifyPropertyChanged("children");
}
}
}
private void ChildOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
parentAmount = children.Sum(p => p.ChildAmount);
}
}
class ChildViewModel : BaseViewModel
{
private double _ChildAmount;
public double ChildAmount
{
get { return _ChildAmount; }
set
{
if (value != _ChildAmount)
{
_ChildAmount = value;
NotifyPropertyChanged("ChildAmount");
}
}
}
}
public class BaseViewModel : INotifyPropertyChanged
{
protected void NotifyPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
A more elegant way is to use Reactive Extensions.
First you need to grab
Rx-Main
from Package Manager Console.
Then, create a static class to host your extension method implemented using Rx. Something like this -
public static class Extensions
{
public static IObservable<T> OnPropertyChanges<T>(this T source, string propertyName)
where T : INotifyPropertyChanged
{
return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
handler => handler.Invoke,
h => source.PropertyChanged += h,
h => source.PropertyChanged -= h)
.Where(p => p.EventArgs.PropertyName == propertyName)
.Select(_ => source);
}
}
Lastly, you call this method in your ParentViewModel's constructor (or anywhere necessary).
// whenever the ChildAmount property of any ChildViewModel has changed, do something
Observable.Merge(children.Select(c => c.OnPropertyChanges("ChildAmount")))
.Subscribe((c) =>
{
// update your parent amount here
NotifyPropertyChanged("parentAmount");
});

Exposing a property of a child view model in the parent view model

my parent view model contains couple child view models and it looks like
public MainViewModel:ObservableObject
{
public MainViewModel(){//initalize everything};
private SomeViewModel childvm1;
private AnotherViewModel childvm2;
public SomeViewModel Childvm1
{
get
{
return childvm1;
}
set
{
SetField(ref childvm1, value, "Childvm1");
}
}
public AnotherViewModel Childvm2
{
get
{
return childvm2;
}
set
{
SetField(ref childvm2, value, "Childvm2");
}
}
//when this changes i want to notify childvm2 and call a function in it
public SomeModel SelectedValueofChildvm1
{
get
{
return Childvm1.SelectedValue;
}
}
}
how can i call a function in childvm2 when SelectedValueofChildvm1 changes?
You have to subscribe to the PropertyChangedEvent of the child view model, like this:
public SomeViewModel Childvm1
{
get
{
return childvm1;
}
set
{
if (childvm1 != null) childvm1.PropertyChanged -= OnChildvm1PropertyChanged;
SetField(ref childvm1, value, "Childvm1");
if (childvm1 != null) childvm1.PropertyChanged += OnChildvm1PropertyChanged;
}
}
private coid OnChildvm1PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// now update Childvm2
}
But be careful:
you may have to update childvm2 also in the Childvm2 setter
you need to make sure that childvm1 instances don't outlive MianViewModel instances, or set Childvm1 to null before giving the MainViewModel back to the garbage collector.
This easiest way is to use the INotifyPropertyChanged interface to listen for the property change notifications.
public MainViewModel:ObservableObject
{
public MainViewModel(){
//initalize everything
Childvm1.PropertyChanged += (s,e) {
if(e.PropertyName == "SelectedValue") {
// Do what you want
}
};
};
}

DataGrid - change edit behaviour

I have an ObservableCollection of ChildViewModels with somewhat complex behaviour.
When I go to edit a row - the DataGrid goes into 'edit-mode' - this effectively disables UI-notifications outside the current cell until the row is committed - is this intended behaviour and more importantly can it be changed?
Example:
public class ViewModel
{
public ViewModel()
{
Childs = new ObservableCollection<ChildViewModel> {new ChildViewModel()};
}
public ObservableCollection<ChildViewModel> Childs { get; private set; }
}
public class ChildViewModel : INotifyPropertyChanged
{
private string _firstProperty;
public string FirstProperty
{
get { return _firstProperty; }
set
{
_firstProperty = value;
_secondProperty = value;
OnPropetyChanged("FirstProperty");
OnPropetyChanged("SecondProperty");
}
}
private string _secondProperty;
public string SecondProperty
{
get { return _secondProperty; }
set
{
_secondProperty = value;
OnPropetyChanged("SecondProperty");
}
}
private void OnPropetyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler PropertyChanged;
}
And in View:
<Window.Resources>
<local:ViewModel x:Key="Data"/>
</Window.Resources>
<DataGrid DataContext="{Binding Source={StaticResource Data}}" ItemsSource="{Binding Childs}"/>
Notice how the second notification when editing first column is hidden until you leave the row.
EDIT: Implementing IEditableObject does nothing:
public class ChildViewModel : INotifyPropertyChanged,IEditableObject
{
...
private ChildViewModel _localCopy;
public void BeginEdit()
{
_localCopy = new ChildViewModel {FirstProperty = FirstProperty, SecondProperty = SecondProperty};
}
public void EndEdit()
{
_localCopy = null;
}
public void CancelEdit()
{
SecondProperty = _localCopy.SecondProperty;
FirstProperty = _localCopy.FirstProperty;
}
}
This behavior is implemented in DataGrid using BindingGroup. The DataGrid sets ItemsControl.ItemBindingGroup in order to apply a BindingGroup to every row. It initializes this in MeasureOverride, so you can override MeasureOverride and clear them out:
public class NoBindingGroupGrid
: DataGrid
{
protected override Size MeasureOverride(Size availableSize)
{
var desiredSize = base.MeasureOverride(availableSize);
ClearBindingGroup();
return desiredSize;
}
private void ClearBindingGroup()
{
// Clear ItemBindingGroup so it isn't applied to new rows
ItemBindingGroup = null;
// Clear BindingGroup on already created rows
foreach (var item in Items)
{
var row = ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
row.BindingGroup = null;
}
}
}
This is very old question, but a much better solution which doesn't require subclassing DataGrid exists. Just call CommitEdit() in the CellEditEnding event:
bool manualCommit = false;
private void MyDataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
if (!manualCommit)
{
manualCommit = true;
MyDataGrid.CommitEdit(DataGridEditingUnit.Row, true);
manualCommit = false;
}
}
ok, so, here is the problem. Observable Collection does NOT notify of objects that it contains changing. It only notifies on add/remove/etc. operations that update the collection is-self.
I had this problem and had to manually add my columns to the datagrid, then set the Binding item on the Column object. so that it would bind to my contents.
Also, I made the objects that are in my ICollectionView derive from IEditableObject so when they are "updated" the grid will refresh itself.
this sucks, but its what i had to do to get it to work.
Optionally, you could make your own ObservableCollection that attaches/detaches property changed handlers when an item is addeed and remove.

Categories