Communication between 2 ListBoxes in WPF - 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;
}
}
}

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();
}

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

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

How to edit elements in a collection from another window or Dialog

I have a number of collection bound to the application main window controls shown in a
simplified form below. There are a number of other elements in the view model
(Ommited for Clarity) which all update and work as expected.
I require to edit a collection's element in another window, with the edited data back in the origonal collection.
/// Example of the Collection and Properties
ObservableCollection<MyData> _MyCollection = new ObservableCollection<MyData>();
public ObservableCollection<MyData> MyCollection { get { return _MyCollection; } }
public class MyData : INotifyPropertyChanged
{
private bool cb_checked;
public string Param1 { get; set; }
public string Param2 { get; set; }
public bool myCheck
{
get { return cb_checked; }
set
{
if (cb_checked == value) return;
cb_checked = value;
RaisePropertyChanged("Checked");
}
}
}
My problem is how do I pass an item of a collection to a new window for editing.
My intal thoughts were to pass the item in the constructor of the window
Dialog.Edit window = new Dialog.Edit(_MyCollection[2] );
window.Owner = this;
window.Show();
I also tried this as I have read I cant use indexed references
var tmp = _MyCollection[2];
Dialog.Edit window = new Dialog.Edit( tmp);
window.Owner = this;
window.Show();
but this does not work and I get null exceptions whe trying to access elements.
If I need to pass the complete collection this is also ok as they are all quite small i.e. < 50 items.
I must be going about this in the wrong way, could someone please explain how to do this
correctly please.
Many Thanks
Sarah

wpf ObservableCollection fill combobox

How do I fill up my combobox to an item of my ObservableCollectio ?
public ObservableCollection<Contacts> contacts = new ObservableCollection<Contacts>();
Item within Contacts is "Grname". Those items need to be binded to it. Prefer by code, because I want to filter out the duplicates (grouping).
class Contacts
{
public string Contact_id { get; set; }
public string Grname { get; set; }
}
UPDATE:
I found it !
ICollectionView contactsView = CollectionViewSource.GetDefaultView(dataGrid1.ItemsSource);
cmbGroup.ItemsSource = contactsView.Groups;
But how to filter my datagrid with the selected item of combobox ?
I've got:
void Filter(object sender, FilterEventArgs e)
{
if (cmbGroup.ItemsSource == contactsView)
{
e.Accepted = true;
}
else
{
e.Accepted = false;
}
}
And Filter is binded in CollectionViewSource in my XAML
For filtering, grouping, sorting etc. you could use a CollectionViewSource. That means something like
ICollectionView contactsView = CollectionViewSource.GetDefaultView(contacts);
// For filtering:
contactsView.Filter += sender => {
// Filter logic here
return true;
}
Then you bind your combobox against the contactsView.

MVVM, two synchronized Grid Data

I want to do a control for adding and removing items from a two list (Selected and UnSelected), like this:
But I can't find a good way to do this; how can I use the GridData (or similar control like GridControl of Devexpress) for binding two List and modify it?
Problems:
I can't use ObservableCollection with this control
I can't bind the SelectedItems
If you have any suggestion or sample for some work, it will be a great help
Can you use two observable collections? One for selected and one for unselected. It seems to be the easiest way to implement such functionality.
public class MainViewModel
{
private readonly ObservableCollection<Item> _selectedItems = new ObservableCollection();
private readonly ObservableCollection<Item> _unselectedItems = new ObservableCollection();
public IEnumerable<Item> SelectedItems { get { return _selectedItems; } }
public IEnumerable<Item> UnselectedItems { get { return _unselectedItems; } }
private void UnselectItems()
{
MoveFromOneCollectionToAnother(_unselectedItems, _selectedItems, ...);
}
private void SelectItems()
{
MoveFromOneCollectionToAnother(_selectedItems, _unselectedItems, ...);
}
private void MoveFromOneCollectionToAnother(ICollection<Item> source, ICollection<Item> destination, IEnumerable<Item> itemsToMove)
{
foreach (var item in itemsToMove)
{
source.Remove(item);
destination.Add(item);
}
}
}

Resources