Update ObservableCollection item and related view, when the item implements an interface - wpf

As written everywhere (e.g. here, here, here, here...), reporting an ObservableCollection item property change to a view requires the item to implement INotifyPropertyChanged.
Using CommunityToolkit.Mvvm this can be done using an attribute:
public class MyViewModel : ObservableObject
{
public ObservableCollection<MyItem> MyCollection { get; set; }
//...
}
[INotifyPropertyChanged] // yay! No boilerplate code needed
public partial class MyItem
{
public string MyProperty { get; set; }
}
If somewhere inside MyViewModel there is a change to the MyProperty of an item of MyCollection, the view will be updated.
What if an interface comes into play?
public class MyViewModel : ObservableObject
{
public ObservableCollection<IMyInterface> MyCollection { get; set; }
//...
}
[INotifyPropertyChanged]
public partial class MyItem : IMyInterface // MyProperty is in IMyInterface too
{
public string MyProperty { get; set; }
}
The view seems not to be updated anymore.
I tried:
Inheriting INotifyPropertyChanged in IMyInterface (that requires explicit implementation of the PropertyChanged event and OnPropertyMethod method in MyItem, which I don't want as otherwise I would have not used CommunityToolkit.Mvvm)
Adding [INotifyPropertyChanged] to MyViewModel (expecting nothing but there was an answer somewhere telling that)
Is there an obvious, no-boilerplate solution that I'm missing here?
The view is updated if I do something like suggested here
var item = MyCollection[0];
item.MyProperty = "new value";
MyCollection[0] = item;
but I hope there's a better way.

I'm not sure this will solve your issue but I can see you're using the attribute incorrectly.
Please see
https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/inotifypropertychanged
"These attributes are only meant to be used in cases where the target types cannot just inherit from the equivalent types (eg. from ObservableObject). If that is possible, inheriting is the recommended approach, as it will reduce the binary size by avoiding creating duplicated code into the final assembly."
You're not inheriting from IMyInterface, you're implementing it.
Your viewmodel should inherit ObservableObject
Instead of
[INotifyPropertyChanged]
public partial class MyItem : IMyInterface
You should have:
public partial class MyItem : ObservableObject, IMyInterface
Properties look like:
[ObservableProperty]
private List<FlatTransactionVM> transactions = new List<FlatTransactionVM>();
That generates a public Transactions property.
If you only have properties, you don't need that to be a partial class. That's necessary for relaycommand generation.
EDIT
This works for me:
MainWindow
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding MyCollection}"
DisplayMemberPath="MyProperty"/>
</Grid>
MainWindowViewModel
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection<IMyInterface> myCollection = new ObservableCollection<IMyInterface>();
public MainWindowViewModel()
{
MyCollection = new ObservableCollection<IMyInterface>(
new List<MyItem>{
new MyItem{ MyProperty = "A" },
new MyItem { MyProperty = "B" },
new MyItem { MyProperty = "C" }
});
}
}
MyItem
public partial class MyItem : ObservableObject, IMyInterface
{
[ObservableProperty]
private string myProperty;
}
Interface
public interface IMyInterface
{
string MyProperty { get; set; }
}
Quick and dirty code in the view. This is purely to see what happens when one of the MyItems.MyProperty is set.
private async void Window_ContentRendered(object sender, EventArgs e)
{
await Task.Delay(5000);
var mw = this.DataContext as MainWindowViewModel;
mw.MyCollection[1].MyProperty = "XXXXX";
}
After a short wait, I see the second item change to XXXXX as expected.

Related

Binded property not refreshing while event called from another User Control

To simplify situation. I have MainWindow with two user controls, all of them have corresponding Viewmodels. Everything works fine, properties bind and so on, beside one functionality.
I want to refresh data on second user control after event happened in the first one. Unfortunetly in this scenario, PropertyChanged event (derived from INotifyPropertyChanged, defined in ViewModelBase) is null.
However, if I raise an event from second user control, property on view gets updated as expected!
public class MainWindowViewModel : ViewModelBase
{
public FirstUserControl FirstUserControl {get; set;}
public SecondUserControl SecondUserControl {get; set;}
public MainWindowViewModel ()
{
FirstUserControl =new FirstUserControl();
FirstUserControl.RaiseClicked+=OnRaiseClicked;
SecondUserControl = new SecondUserControl();
SecondUserControl .RaiseClicked+=OnRaiseClicked;
}
private void OnRaiseClicked(object sender, EventArgs e)
{
SecondUserControl.RefreshView();
}
}
public class FirstUserControl : ViewModelBase
{
public ICommand Raise { get; private set; }
public EventHandler RaiseClicked {get;set;}
public FirstUserControl ()
{
Raise = new RelayCommand( p=> RaiseClicked(this, null));
}
}
public class SecondUserControl: ViewModelBase
{
public ICommand Raise { get; private set; }
public EventHandler RaiseClicked {get;set;}
public string Title
{
get
{
return MyLogic.GetCurrentTitle(); // debuggers enter here only while event on second user control raised
}
}
public void RefreshView()
{
OnPropertyChanged("Title"); // debugger enter here in cases
}
}
I suppose there is something with threads going on, but I'm not that familiar with WPF to work out it by my own. Can someone help how to quickly and easy make event from first UC refresh data on the second?

WPF binding static list to combobox

Trying to understand how to bind this static list to a combobox that located on different window.
public partial class MainWindow : Window
{
public static List<Classes.Entity> EntityList { get; set; }
public MainWindow()
{
EntityList = new List<Classes.Entity>();
InitializeComponent();
}
...
the object:
public class Entity
{
public string entityName { get; set; }
...
XAML (In a diffrent window, call "NewRelationship.xaml.cs"
<ComboBox x:Name="cb_from" ItemsSource="{Binding Path=EntityList}" DisplayMemberPath="entityName" SelectedValue="{Binding Path=Entity}" />
Of course I fill the list later in the code...
if I moving the list to the newRelationship window and add "this.datacontext = this;" its working,
How do I make this work when the list is in the mainWindow? Thanks...
A better approach would be to keep the EntityList in a separate object that both windows could reference:
class ViewModel
{
private List<Classes.Entity> _entityList = new List<Classes.Entity>();
public IEnumerable<Classes.Entity> EntityList
{
get { return _entityList; }
}
}
partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
When the second window is created, you can pass an instance of the ViewModel class to it, and set it as the DataContext.

Using IDataErrorInfo in Model V/s ViewModel

I'm having trouble trying to figure out the best way to implement IDataErrorInfo in my WPF application. I have a few models, each with a lot of properties, that are being used. The ViewModel has Properties of each of these models and the View uses these properties to bind to the model. This is a reduced version of the structure I have:
public class ModelA
{
public string PropertyA1 {get; set;}
public string PropertyA2 {get; set;}
}
public class ModelB
{
public string Property B1 {get; set;}
public string Property B2 {get; set;}
}
public class ViewModel
{
public ModelA modelA {get; set;}
public ModelB modelB {get; set;}
}
My questions is - Where do I implement IDataErrorInfo - In the ViewModel or in the Model?
The View binds to these properties as modelA.PropertyA1 and so on so the errors are raised in the Models and not in the ViewModel which makes it necessary to implement IDataErrorInfo in the Models. However, I've heard that it's a good practice to implement validation logic in the ViewModel.
I was wondering if there was a way to catch the errors in the ViewModel without writing a wrapper for each of the properties that would raise an error as there are lots of properties and writing a wrapper for each of them would be tedious.
Thanks for your help!
I think you should implement IDataErrorInfo in the view-model. If you have a base class for your view-models, and you probably should, you can implement it there. Now, all your implementation/validation-logic is in your base class and not spread amongst n-number of view-models.
You want a Valaidation of an Property of an Model this can only be done if you are implement in a Base class or for each Model in the model. If you want to Validate ModelA in ViewModel you need to impliment it in ViewModel but if you want to Implement a Validation for A1 you are need to Implement it in ModelA.
If you change the instance of modelA, IDataViewModel will go into the class and try to call (Instance of your ViewModel)["modelA"]. OK but this is not what you want, if you change a property of modelA IDataViewModel will go into the Instance of modelA and call modelA["B1"], and if you are now implement your validation in ViewModel, modelA will retrun an Empty string
Thank you for your help guys! Here's how I decided to solve my problem:
I created two base classes, one for normal models (Ones that don't have any validations. It only implements INotifyPropertyChanged) and another for models that have validations as shown below.
public abstract class ModelBase : INotifyPropertyChanged
{
//Implement INotifyPropertyChanged here
}
public delegate string ValidateProperty(string propertyName);
public abstract class ValidationModelBase : ModelBase, IDataErrorInfo
{
private bool _canValidate;
public bool CanValidate
{
get { return _canValidate; }
set { _canValidate = value; }
}
#region IDataErrorInfo Members
public string Error
{
get { return string.Empty; }
}
public string this[string columnName]
{
get
{
if (this.CanValidate)
{
return this.Validate(columnName);
}
return string.Empty;
}
}
#endregion
#region Validation Section
public event ValidateProperty OnValidateProperty;
public string Validate(string propertyName)
{
if (this.OnValidateProperty != null)
{
return OnValidateProperty(propertyName);
}
return string.Empty;
}
#endregion
}
Now my models looked like this:
public class ModelA : validationModelBase
{
public string PropertyA1 {get; set;}
public string PropertyA2 {get; set;}
}
public class ModelB : ValidationModelBase
{
public string Property B1 {get; set;}
public string Property B2 {get; set;}
}
Not a huge change there. The ViewModel now looks like this:
public class ViewModel
{
public ModelA modelA {get; set;}
public ModelB modelB {get; set;}
public ViewModel()
{
this.modelA.OnValidateProperty += new ValidateProperty(ValidateModelA);
this.modelB.OnValidateProperty += new ValidateProperty(ValidateModelB);
}
private string ValidateModelA(string propertyName)
{
//Implement validation logic for ModelA here
}
private string ValidateModelB(string propertyName)
{
//Implement validation logic for ModelB here
}
}
This seems to be working for me so far. This way, any new models that have validation need only derive from ValidationModelBase and have the ViewModel add an event handler for the validation event.
If anyone has a better way of solving my problem, do let me know - I'm open to suggestions and improvements.

Why won't MEF Lazy load work in the ViewModel?

I'm trying to get Lazy to work for a collection in my ViewModel that I'm binding to. The collection loads through MEF fine, but never gets displayed in the bound UI.
Here's the UI:
<Window x:Class="TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<ItemsControl ItemsSource="{Binding MyList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Content="{Binding ItemTitle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel>
</Window>
The code-behind class:
public partial class TestWindow : Window
{
public TestWindow()
{
InitializeComponent();
this.DataContext = new TestVM();
}
}
The ViewModel:
public class TestVM : INotifyPropertyChanged, IPartImportsSatisfiedNotification
{
public TestVM()
{
//I'm using a static class to initiate the import
CompositionInitializer.SatisfyImports(this);
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
[ImportMany(typeof(MyItemBase))]
public Lazy<MyItemBase>[] MyList { get; set; }
public void OnImportsSatisfied()
{
this.PropertyChanged(this, new PropertyChangedEventArgs("MyList"));
}
}
The base class for the items, and some inherited test classes:
[InheritedExport]
public class MyItemBase
{
public MyItemBase()
{
}
public string ItemTitle{ get; set; }
}
public class MyItem1: MyItemBase
{
public MyItem1()
{
this.ItemTitle = "Item 1";
}
}
public class MyItem2: MyItemBase
{
public MyItem2()
{
this.ItemTitle = "Item 2";
}
}
This works IF I just remove the Lazy loading. However, I'll need to apply some export attributes later, which means going to Lazy.
the problem is that you want bind to a list of MyItembase object, but your actual binding is to a lazy arrray of MyItembase objects.(as long as you never call .Value for your lazy item nothing will happen)
i my projects i use a private lazy collection for mef and a normal ObservableCollection for wpf. btw i would prefer Constructor injection for your Mef import
public class TestVM : INotifyPropertyChanged, IPartImportsSatisfiedNotification
{
public TestVM()
{
//I'm using a static class to initiate the import
CompositionInitializer.SatisfyImports(this);
this.MyList = new ObservableCollection();
foreach(var lazyitem in _mefList)
{
this.MyList.Add(lazyitem.Value);
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public ObservbableCollection<MyItemBase> MyList{ get; set; }
[ImportMany(typeof(MyItemBase))]
private IEnumarable<Lazy<MyItemBase>> _mefList { get; set; }
public void OnImportsSatisfied()
{
//this.PropertyChanged(this, new PropertyChangedEventArgs("MyList"));
}
}

Silverlight Wcf Ria service viewmodel combobox

Ok I'll make this very simple! Here are viewmodels :
public class ObjectsModel
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string _objectName;
public string ObjectName
{
get
{
return _objectName;
}
set
{
if (value != _objectName)
{
_objectName = value;
PropertyChanged(this, new PropertyChangedEventArgs("ObjectName"));
}
}
}
public IEnumerable<Object> Objects {get;set;}
public ICommand AddCommand { get; private set; }
public ICommand SaveChangesCommand { get; private set; }
myDomainContext context = new myDomainContext();
public ObjectsModel()
{
objects = context.Objects;
context.Load(context.GetObjectsQuery());
}
}
public class InventoryModel
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public IEnumerable<Inventory> Inventories {get;set;}
public ICommand AddCommand { get; private set; }
public ICommand SaveChangesCommand { get; private set; }
myDomainContext context = new myDomainContext();
public ObjectsModel()
{
objects = context.Objects;
context.Load(context.GetObjectsQuery());
}
}
So what I'm trying to do is in my second form where I want to add an inventory for an object, I have to select the object in a combobox. The question is, how do I fill my combobox? Create another instance of the "ObjectsModel" in the InventoryModel? or use another "context" where I would query the other table? Or is there an easier Xaml approach? If I'm not clear, tell me I'll put more examples/code.
tx a lot!
You want to bind the contents of the combobox to a list of items provided by your ViewModel and bind the selected item to another property on the same ViewModel.
Please get into the habit of naming actual view models to end in "ViewModel", rather than "Model", so they do not clash with your other "real" models. It actually looks like you are binding directly to your business models instead of ViewModels (which is not good).

Resources