How to use ReactiveList so UI is updated when items are added/removed/modified - winforms

I'm creating a WinForms application with a DataGridView. The DataSource is a ReactiveList. Adding new items to the list however does not update the UI.
ViewModel
public class HomeViewModel: ReactiveObject
{
public ReactiveCommand<object> AddCmd { get; private set; }
ReactiveList<Model> _models;
public ReactiveList<Model> Models
{
get { return _models; }
set { this.RaiseAndSetIfChanged(ref _models, value); }
}
public HomeViewModel()
{
Models = new ReactiveList<Model>() { new Model { Name = "John" } };
AddCmd = ReactiveCommand.Create();
AddCmd.ObserveOn(RxApp.MainThreadScheduler);
AddCmd.Subscribe( _ =>
{
Models.Add(new Model { Name = "Martha" });
});
}
}
public class Model
{
public string Name { get; set; }
}
View
public partial class HomeView : Form, IViewFor<HomeViewModel>
{
public HomeView()
{
InitializeComponent();
VM = new HomeViewModel();
this.OneWayBind(VM, x => x.Models, x => x.gvData.DataSource);
this.BindCommand(VM, x => x.AddCmd, x => x.cmdAdd);
}
public HomeViewModel VM { get; set; }
object IViewFor.ViewModel
{
get { return VM; }
set { VM = (HomeViewModel)value; }
}
HomeViewModel IViewFor<HomeViewModel>.ViewModel
{
get { return VM; }
set { VM = value; }
}
}
The view always show "John".
Debugging Subscribe show added items.
Tried it with ObservableCollection same result.How to use ReactiveList so UI is updated when new items are added
Tried it with IReactiveDerivedList same result. Does ReactiveUI RaiseAndSetIfChanged fire for List<T> Add, Delete, Modify?

I think what you want is a ReactiveBindingList rather than a ReactiveList. This is a WinForms specific version of the ReactiveList for binding purposes.

You should use BindingList.
reference :
"If you are bound to a data source that does not implement the IBindingList interface, such as an ArrayList, the bound control's data will not be updated when the data source is updated. For example, if you have a combo box bound to an ArrayList and data is added to the ArrayList, these new items will not appear in the combo box. However, you can force the combo box to be updated by calling the SuspendBinding and ResumeBinding methods on the instance of the BindingContext class to which the control is bound."
https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-bind-a-windows-forms-combobox-or-listbox-control-to-data?view=netframeworkdesktop-4.8
Or
ReactiveBindingList
It work fine for me. !!!

Related

ObservableCollection deep cloning

I've implemented deep cloning of ObservableCollection in order to reset items to It's original state in editable Datagrid, via cancel button.
For this I have two collections - one ObservableCollection to bind Datagrid to It, and cloned List to re-initialize ObservableCollection to It's original state when needed.
My code works only first time I hit a cancel button, after that my cloned List has changes in It too.
Provided code is an example (mine is a bit longer), but It's 100% same as mine:
Model, which implements ICloneable:
public class EmployeeModel : ICloneable
{
public object Clone()
{
return MemberwiseClone();
}
public string NAME
{
get { return _name; }
set
{
if (_name != value)
{
CHANGE = true;
_name = value;
}
}
}
private string _name;
public string SURNAME
{
get { return _surname; }
set
{
if (_surname != value)
{
CHANGE = true;
_surname = value;
}
}
}
private string _surname;
///<summary>Property for tracking changes in model</summary>
public bool CHANGE { get; set; }
}
Viewmodel:
public ViewModel() : Base //Implements InotifyPropertyChanged
{
public ViewModel()
{
Task.Run(()=> GetData());
}
public ObservableCollection<EmployeeModel> Employees
{
get { return _employees; }
set { _employees = value; OnPropertyChanged();}
}
private ObservableCollection<EmployeeModel> _employees;
public List<EmployeeModel> Copy_employees
{
get { return _copy_employees; }
set { _copy_employees = value; OnPropertyChanged();}
}
private List<EmployeeModel> _copy_employees;
//Fetch data from DB
private async Task Get_data()
{
//Returns new ObservableCollection of type Employee
Employees = await _procedures.Get_employees();
if (Employees != null) //Now make a deep copy of Collection
{
Copy_employees = new List<EmployeeModel>();
Copy_employees = Employees.Select(s => (EmployeeModel)s.Clone()).ToList();
}
}
//My Command for canceling changes (reseting DataGrid)
//CanExecute happens, when model is changed - tracking via CHANGE property of EmployeeModel
public void Cancel_Execute(object parameter)
{
Employees.Clear(); //Tried with re-initializing too, but same result
foreach (var item in Copy_employees)// Reset binded ObservableCollection with old items
{
Employees.Add(item);
}
//Check if copied List really hasn't got any changes
foreach (EmployeeModel item in Copy_employees)
{
Console.WriteLine("Changes are " + item.CHANGES.ToString());
}
}
}
Output of cancel command:
1.) First time I hit cancel button:
// Changes are False
Every next time:
// Changes are True
So, as I see It from Console, my copied List get's updated when ObservableColection get's updated, even if It's not binded to DataGrid.
And It updates only a property which I changed, so List reflects ObservableCollection items.
How can I keep my original items of List<Employee>, and copy those into binded ObservableCollection anytime ?
When you return values, you do not return them, but write backing item references to the editable collection.
As a result, you have the same instances in both collections.
In the simplest case, when you return them, you also need to clone.
public void Cancel_Execute(object parameter)
{
Employees.Clear(); //Tried with re-initializing too, but same result
foreach (var item in Copy_employees)// Reset binded ObservableCollection with old items
{
Employees.Add((EmployeeModel)item.Clone());
}
//Check if copied List really hasn't got any changes
foreach (EmployeeModel item in Copy_employees)
{
Console.WriteLine("Changes are " + item.CHANGES.ToString());
}
}
Not relevant to the question, but I still advise you to use a slightly more user-friendly interface for cloneable:
public interface ICloneable<T> : ICloneable
{
new T Clone();
}

Databinding not refreshed in Xamarin (WPF)

I have some trouble with the data binding while using MVVM in Xamarin. Let me explain my architecture:
I have a Manager-class, which contains a ObservableCollection with classes of type t, t is my model class. The Manager-class contains also a attribute called activeT, which is the current selected object of my model class. There are 2 UIs, one which shows the current data of t. The viewModel is bound to the attribute t of my Manager-class like that:
public t CurrentT
{
get
{
return _mgr.CurrentT;
}
set
{
_mgr.CurrentT = value;
OnPropertyChanged();
}
}
_mgr is my singleton Manager-Object.
Now there is the other view, which is able to choose the current t out of a combobox. The viewModel of that view is bound to the ObservableCollection of the manager. If I change the selected object, I do it like with the same code like above. The Property of the manager is the following code:
public t CurrentT
{
get
{
return _currentT;
}
set
{
_currentT= value;
OnPropertyChanged());
}
}
The problem is now, that the first view to view the current selected t does not refresh, though I can see in the debugger, that the current t is changed by the other view.
Can someone help me?
Edit:
I provide some more Code:
The Manager-Class:
public class Manager : INotifyPropertyChanged
{
private t _currentConstructionSite;
private ObservableCollection<t> _constructionSites = null;
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public t CurrentConstructionSite
{
get
{
return _currentConstructionSite;
}
set
{
_currentConstructionSite = value;
OnPropertyChanged("CurrentConstructionSite");
}
}
public ObservableCollection<t> ConstructionSites
{
get
{
return _constructionSites;
}
set
{
_constructionSites = value;
}
}
private Manager()
{
ConstructionSites = DataRepository.GenConstructionSites();
_currentConstructionSite = ConstructionSites[0];
}
}
The ViewModels Class A (This is the viewmodel of the view, which shows some data):
public class DashboardViewModel : ViewModelBase
{
private Manager _mgr;
public t CurrentConstructionSite
{
get
{
return _mgr.CurrentConstructionSite;
}
set
{
_mgr.CurrentConstructionSite = value;
OnPropertyChanged();
}
}
public DashboardViewModel()
{
_mgr = Manager.getInstance();
}
}
The View A to show some data:
Binding Setup from XAML:
<ContentPage.BindingContext>
<local:DashboardViewModel x:Name="viewModel"/>
</ContentPage.BindingContext>
Binding a Label to show data:
<Label Text="{Binding CurrentConstructionSite.ConstructionSiteName, Mode=TwoWay}" HorizontalOptions="Center" Font="Bold" FontSize="Large"/>
ViewModel B to choose the current t:
public class ChooseConstructionSiteViewModel : ViewModelBase
{
Manager _mgr = null;
public ObservableCollection<t> ConstructionSites
{
get
{
return _mgr.ConstructionSites;
}
}
public t CurrentConstructionSite
{
get
{
return _mgr.CurrentConstructionSite;
}
set
{
_mgr.CurrentConstructionSite = value;
OnPropertyChanged();
}
}
public ChooseConstructionSiteViewModel()
{
_mgr = Manager.getInstance();
}
}
The View to choose the current t:
<combobox:SfComboBox x:Name="combobox" Grid.Row="0" Grid.Column="1" Margin="8,0,20,0" VerticalOptions="Center" HeightRequest="40" DataSource="{Binding ConstructionSites}" DisplayMemberPath="ConstructionSiteName" SelectionChanged="Handle_SelectionChanged"/>
And if the selection from the combobox changed:
void Handle_SelectionChanged(object sender, Syncfusion.XForms.ComboBox.SelectionChangedEventArgs e)
{
t selectedItem = e.Value as t;
_viewModel.CurrentConstructionSite = selectedItem;
}
The two views are contained as contetPages in a tabbedPage. It works in general, but the changing the selected t in the view B does not update the data in view A. I can see in the debugger that the value of t is changed via view B but when I go back to view A there is the old value. In the debugger I can see that the value is updated.
BTW: ViewModelBase is the class which implements INotifyPropertyChanged
From you second viewmodel, you need to "notify" the first viewmodel that the data has changed.
One of the ways to do that, would be to use a Messenger (MvvmLight has one, so does MvvmCross). You can then use the messenger from your Manager, to notify all the viewmodels that need the info, that CurrentT changed.
In the messenger subscription in your viewmodels, simply call a RaiseNotifyPropertyChanged of your property, and you should be good to go

How to inform a ViewModel that its collectionSource was changed(add, update or delete) from another viewModel in WPF MVVM?

If i have a viewmodel for adding new item and another viewmodel for displaying all or filtered items (the second viewmodel's view must always reflect any changes in the collection source), should the change be communicated from viewmodel to viewmodel directly, or viewmodel to repository to second viewmodel?
I tried the following
public class DataAccess //my repository
{
public DataAccess()
{
var ctx = new MyDbContext();
}
public void AddNewItem(Item item)
{
ctx.items.Add(item);
ctx.SaveChanges();
}
public ObservableCollection<Item> GetAllItems()
{
return new ObservableCollection<Item>(ctx.items.ToList());
}
Here's my first ViewModel
public class ItemsViewModel : ObservableObject
{
public ItemsViewModel()
{
DataAccess dt = new DataAccess();
AllItems = new ObservableCollection<Item>(dt.GetAllItems());
}
//AllItems is bound to datagrid
private ObservableCollection<Item> _allItems;
public ObservableCollection<Item> AllItems
{
get {return _allItems;}
set {_allItems = value; RaisePropertyChanged();}
}
//command to load the form for adding new item then the method below
//I passed AllItems into the constructor
//I think I can also pass AllItems using Messenger, but I haven't tried it yet
void LoadNewItemForm()
{
NewItemView view = new NewItemView(){DataContext = new NewItemViewModel(AllItems)};
view.ShowDialog();
}
Here's the second viewmodel
public class AddNewItemViewModel : ObservableObject
{
public AddNewItemViewModel(ObservableCollection<Item> allItems)
{
DataAccess dt = new DataAccess();
_allItems = allItems;
}
private ObservableCollection<Item> _allItems;
public ObservableCollection<Item> AllItems
{
get {return _allItems;}
set {_allItems = value; RaisePropertyChanged();}
}
//Here's the AddNewItem method
public void SaveNewItem()
{
Item newitem = new Item(){ ..... };
dt.AddNewItem(item);
//Now is this change in AllItems here supposed to reflect in the allItems passed via constructor (will this reflect in the first ViewModel)
AllItems.Add(newitem);
}
I've tried creating the AllItems property in the repository (DataAccess class) so that this AllItems property will be the one to be returned in the call for GetAllItems() and also it is in the repository where I'll call AllItems.Add(something) or AllItems.Remove(something). Still, change in this does not reflect in the first viewModel.
Here's an example of using events to inform the view models when the data changes.
The data layer:
public class DataAccess
{
private readonly MyDbContext ctx = new MyDbContext();
public event EventHandler<DataChangedEventArgs> ItemsChanged;
public void AddNewItem(Item item)
{
ctx.Items.Add(item);
ctx.SaveChanges();
RaiseItemsChanged(new DataChangedEventArgs(DataAction.Added, item));
}
public List<Item> GetAllItems()
{
return ctx.Items.ToList();
}
public void RaiseItemsChanged(DataChangedEventArgs eventArgs)
{
ItemsChanged?.Invoke(this, eventArgs);
}
}
The event args:
public enum DataAction
{
Added,
Deleted
}
public class DataChangedEventArgs
{
public DataAction DataAction { get; set; }
public Item Item { get; set; }
public DataChangedEventArgs(DataAction dataAction, Item item)
{
DataAction = dataAction;
Item = item;
}
}
The first view model:
public class ItemsViewModel
{
private readonly DataAccess dataAccess = new DataAccess();
public ObservableCollection<Item> AllItems { get; set; }
public ItemsViewModel()
{
AllItems = new ObservableCollection<Item>(dataAccess.GetAllItems());
dataAccess.ItemsChanged += (sender, eventArgs) =>
{
if (eventArgs.DataAction == DataAction.Added)
AllItems.Add(eventArgs.Item);
else
AllItems.Remove(eventArgs.Item);
};
}
}
This helps separate concerns because the AddNewItemViewModel can now add an item without worrying about updating ItemsViewModel. You could also take a lazier approach and handle any event by clearing and repopulating the ObservableCollection<Item>.

MVVM & business logic Layer

I have a problem with MVVM pattern and binding collection. My ViewModel provides a collection to the View but to get this collection I use this:
public BindingList<Car> BindingListCars { get; set; }
public CarsVm()
{
BindingListVoiture = carServices.ListCars;
}
When I bind my View on this List it's as if I bind directly my View on the Model because they use the same reference. So when I edit one property of a Car, the model is directly edited without using carServices validation method.
What is the best solution to correct this problem ?
Do I have to expose a copy of my Model to my View to not edit directly my Model from the View?
Do I have to use BindingList in my Model and subsribe to ListChanged in my carServices to validate each change?
You should either perform the validation directly in the Car class itself or expose wrapper objects instead of exposing the "real" Car objects to the view.
The following sample code should give you the idea about what I mean:
//the "pure" model class:
public class Car
{
public string Model { get; set; }
}
public class CarService
{
public List<CarWrapper> ListCar()
{
List<Car> cars = new List<Car>(); //get your Car objects...
return cars.Select(c => new CarWrapper(c, this)).ToList();
}
public bool Validate()
{
//
return true;
}
}
public class CarWrapper
{
private readonly Car _model;
CarService _service;
public CarWrapper(Car model, CarService service)
{
_model = model;
_service = service;
}
//create a wrapper property for each property of the Car model:
public string Model
{
get { return _model.Model; }
set
{
if(_service.Validate())
_model.Model = value;
}
}
}
Obviously if you expose an IEnumerable<Car> from your view model for the view to bind, you are effectively bypassing any validation that is dedined outside of the Car class if the view is able to set any properties of the Car class.
Thanks for your answer mm8,
With this solution I have to create one wrapper per class which need outside validation. It add work and during refactoring we have to edit the class and the Wrapper.
What do you think about this solution :
I put my list of vehicle in a binding list
My service subscribe to ListChanged event of this list
My service implement INotifyDataErrorInfo
For each modification in this list validation is executed
If there is an error ErrorsChanged event is raised
The view model subsribe to this event and retrieve error Data.
The view model subsribe to this event and retrieve error Data.
For example :
My services implementation :
public class VehicleServices : INotifyDataErrorInfo
{
private BindingList<Vehicle> _bindingListCar
public BindingList<Vehicle> BindingListCar
{
get return _bindingListCar;
}
private readonly Dictionary<string, ICollection<string>>
_validationErrors = new Dictionary<string, ICollection<string>>();
//INotifyDataErrorInfo implementation
public IEnumerable GetErrors(string propertyName)
public bool HasErrors
private void RaiseErrorsChanged(string propertyName)
public VehicleServices()
{
_bindingListCar = GetVehicles();
_bindingListCar.ListChanged += BindingListVehicleChanged;
}
private void BindingListVehicleChanged(object sender, ListChangedEventArgs e)
{
//Only modification is managed
if (e.ListChangedType != ListChangedType.ItemChanged) return;
switch(e.PropertyDescriptor.Name)
//Validate each property
//if there is ErrorsChanged is raised
}
}
And my ViewModel
public class CarVm : BindableBase
{
private ICollection<string> _errors;
public ICollection<string> Error
{
get
{
return _errors;
}
set
{
SetProperty(ref _errors, value);
}
}
private VehicleServices _carServices;
public BindingList<Vehicle> BindingListCar { get; set; }
public CarVm(VehicleServices carServices)
{
_carServices = carServices;
BindingListCar = new BindingList<Vehicle>(_carServices.BindingListCar);
_carServices.ErrorsChanged += _carServices_ErrorsChanged;
}
private void _carServices_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
Error = _carServices.ValidationErrors[e.PropertyName];
}
}
Do you think this is a good practice ?

Communication between 2 ListBoxes in WPF

I want to build a WPF backend app for a shop. And one view should contain 2 listboxes. 1 for the items which one can buy and 1 for the categories.
I want to grey out items based on selection. Now more details:
So far my view model has an ObservableCollection<ShopItem>
and the class ShopItem has a price, title and a list of Categories
I want to bind ShopItems to 1 ListBox and the Distinct Category to another 2nd ListBox
Since a ShopItem can have multiple Categories I want to gray out all other categories beside the one that belong to the Selected ShopItem. So selection in my first listbox should control appereance in my 2nd listbox.
On the other hand side, when I select a category I would like to gray out all other ShopItems beside the ones that belong to that category. So again listbox 2 should also affect appereance in listbox 1.
With "grayed out" I mean the items should have another style.
I saw something about MultiTrigger that can swap out Template Styling based on conditions.
I am not sure if I can just bind my ObservableCollection<ShopItem> or would need to have two lists here. Do I need some pub/sub between the two lists. I would like to avoid to foreach over all elements in viewmodel each selection changes, any thoughts here?
I'm scratching my head how to solve this right now. Any suggestions would be great...
I am not sure you can get away with iterating over a collection as to change the list to grey out then each item in the list must notify of the change. The following is an example of how you could do it. Where there is IsSelected you can define a ValueConverter to change the font colour.
class ViewModel : ViewModelBase
{
//displayed on the first list
public ObservableCollection<ShopItemViewModel> Shops { get; private set; }
//displayed on the second list
public ObservableCollection<CategoryViewModel> AllCategories { get; private set; }
//when the user clicks an item on the first list
private ShopItemViewModel _selectedShop;
public ShopItemViewModel SelectedShop
{
get { return _selectedShop; }
set
{
_selectedShop = value;
RaisePropertyChanged("SelectedShop");
foreach (CategoryViewModel cat in AllCategories)
cat.Refresh();
}
}
//when the user clicks an item on the second list
private CategoryViewModel _selectedCat;
public CategoryViewModel SelectedCategory
{
get { return _selectedCat; }
set
{
_selectedCat = value;
RaisePropertyChanged("SelectedCategory");
foreach (ShopItemViewModel shops in Shops)
shops.Refresh();
}
}
}
class ShopItemViewModel : ViewModelBase
{
public ObservableCollection<CategoryViewModel> Categories { get; private set; }
public ShopItemViewModel(ViewModel vm)
{
_vm = vm;
}
private ViewModel _vm;
public void Refresh()
{
RaisePropertyChanged("IsSelected");
}
public bool IsSelected
{
get
{
if (_vm.SelectedCategory != null)
{
return Categories.Contains(_vm.SelectedCategory);
}
return true;
}
}
}
class CategoryViewModel : ViewModelBase
{
public CategoryViewModel(ViewModel vm)
{
_vm = vm;
}
private ViewModel _vm;
public string Title { get; set; }
public void Refresh()
{
RaisePropertyChanged("IsSelected");
}
public bool IsSelected
{
get
{
if (_vm.SelectedShop != null)
{
return _vm.SelectedShop.Categories.Contains(this);
}
return false;
}
}
}

Resources