I have a static collection of items (say numbers from 1 to 100) that I'm presenting in a ComboBox. I can bind these items to the ComboBox no problem.
My question is how to bind a second ComboBox to a subset of those items. The behavior I want is to have the second ComboBox bound to the subset of items remaining after the first ComboBox is selected. For example the first ComboBox would show 1,2,3...,100. If the number 43 is selected in the first ComboBox then the second ComboBox should show 44,45,...,100.
How can this be accomplished and have the second ComboBox update if the first is changed without a lot of code-behind?
I would do this with a using MVVM pattern.
Create a class that implements INotifyChange and expose three Property.
ICollection FullCollection
int FirstIndex
ICollection PartialCollection
Use this class as DataContext for your Control and bind SelectedIndex of the first combo box to FirstIndex property, ItemSource of first combobox to FullCollection and ItemSource of second collection to PartialCollection (Be sure that SelectedIndex binding mode is Two Way).
Then on set of FirstIndex property set the PartialCollection property as you want.
Remember that you have to use NotifyPropertyChange on set method of each Properties.
Hope this help.
I'd go with MVVM design but if you want to just do testing and want to see quick results without going into design patterns then I'd suggest using something like CLINQ / BLINQ / Obtics framework to leverege the power of LINQ while keeping the results live for the combo box.
Since LoSciamano has already posted a reply on that (while i was posting this!) I won't dive into details of MVVM implementation
I would expose the first collection as an ObservableCollection<T> and the second collection as a bare IEnumerable. You can then use the default collection view to handle the events necessary to re-filter the sub collection.
class FilteredVM : ViewModelBase
{
public ObservableCollection<MyVM> Items { get; private set; }
public IEnumerable<MyVM> SubItems { get; private set; }
public FilteredVM()
{
this.Items = new ObservableCollection<MyVM>();
this.SubItems = Enumerable.Empty<MyVM>();
var view = CollectionViewSource.GetDefaultView(this.Items);
view.CurrentChanged += (sender, e) => { SetupFilter(); };
view.CollectionChanged += (sender, e) => { SetupFilter(); };
}
private void SetupFilter()
{
var view = CollectionViewSource.GetDefaultView(this.Items);
var current = view.CurrentItem;
if (current != null)
{
this.SubItems = this.Items.Where((vm,idx) => idx > view.CurrentPosition);
}
else
{
this.SubItems = Enumerable.Empty<MyVM>();
}
this.OnPropertyChanged("SubItems");
}
}
Alternatively, if you'd like to keep CollectionViewSource out of your VM:
class FilteredVM : ViewModelBase
{
private MyVM selectedItem;
public MyVM SelectedItem
{
get { return this.selectedItem; }
set
{
if (value != this.selectedItem)
{
this.selectedItem = value;
this.OnPropertyChanged("SelectedItem");
this.SetupFilter();
}
}
}
public ObservableCollection<MyVM> Items { get; private set; }
public IEnumerable<MyVM> SubItems { get; private set; }
public FilteredVM()
{
this.Items = new ObservableCollection<MyVM>();
this.SubItems = Enumerable.Empty<MyVM>();
this.Items.CollectionChanged += (sender, e) => { this.SetupFilter(); };
}
private void SetupFilter()
{
if (this.SelectedItem != null)
{
var item = this.SelectedItem; // save for closure
this.SubItems = this.Items.SkipWhile(vm => vm != item).Skip(1);
}
else
{
this.SubItems = Enumerable.Empty<MyVM>();
}
this.OnPropertyChanged("SubItems");
}
}
Keep in mind this will require SelectedItem to be properly bound in the View to the ViewModel. The first approach listed allows the SelectedItem to be bound to anywhere (or nowhere).
Have 2 Observable collections so that when item in the 1st box is selected, it will kick off method which clears and re populates the 2nd collection. As it is observableCollection, it gets reflected in the WPF GUI automatically
Here is a concrete example (as usual, may be improved, but the idea is here) :
Code behind an view model :
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
Initialsource = new ObservableCollection<int>();
for (int i = 0; i < 101; i++)
{
Initialsource.Add(i);
}
}
private int _selectedsourceItem;
public int SelectedsourceItem
{
get { return _selectedsourceItem; }
set
{
_selectedsourceItem = value;
SubsetSource = new ObservableCollection<int>(Initialsource.Where(p => p > _selectedsourceItem));
InvokePropertyChanged(new PropertyChangedEventArgs("SubsetSource"));
InvokePropertyChanged(new PropertyChangedEventArgs("SelectedsourceItem"));
}
}
private ObservableCollection<int> _initialsource;
public ObservableCollection<int> Initialsource
{
get { return _initialsource; }
set
{
_initialsource = value;
InvokePropertyChanged(new PropertyChangedEventArgs("Initialsource"));
}
}
private ObservableCollection<int> _subsetSource;
public ObservableCollection<int> SubsetSource
{
get { return _subsetSource ?? (_subsetSource = new ObservableCollection<int>()); }
set
{
_subsetSource = value;
InvokePropertyChanged(new PropertyChangedEventArgs("SubsetSource"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void InvokePropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
XAML :
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<ComboBox Width="100" ItemsSource="{Binding Initialsource}" SelectedItem="{Binding SelectedsourceItem, Mode=TwoWay}"></ComboBox>
<ComboBox Width="100" ItemsSource="{Binding SubsetSource}"></ComboBox>
</StackPanel>
</Grid>
You can use my ObservableComputations library:
ObservableCollection<Item> itemsSubset = ItemsObservableCollection
.Skiping(fromIndex)
.Taking(itemsInSubsetCount);
itemsSubset reflacts all the changes in ItemsObservableCollection.
Related
I need help. I am kind of new in MVVM
I've got event datagrid selecteditem event in MVVM. After creating a source for grid items(void search) I need populate combobox source as a filter values based on main collection. I used(before MVVM) to work with event subscribing, but can not achieve it here. If I put OnPropertChanged("FamilyFilter") at the end of main function combobox selectionchanged event start working immediately. Here is the code:
public IList<Object> somelist { get; set; }
public CollectionView Items { get; set; }
private string _selectedFamily;
public string SelectedFamily
{ get { return _selectedFamily; }
set { Items = <<filteredcollection>>; OnPropertyChanged("SelectedFamily"); }
}
private List<string> _familyEntries;
public List<string> FamilyEntries
{ get { return _familyEntries; }
set { _familyEntries = value; OnPropertyChanged("FamilyEntries");}
}
public void search()
{
Items = (CollectionView)CollectionViewSource.GetDefaultView(somelist);
_familyEntries = somelist.GroupBy(x => x.Family).Select(z => z.First().Family).ToList();
OnPropertyChanged("Items");
OnPropertyChanged("FamilyEntries");
}
<ComboBox ItemsSource="{Binding FamilyEntries}" SelectedValue="{Binding SelectedFamily}"/>
I try to create a DataGrid for WPF / MVVM which allows to manually select one ore more items from ViewModel code.
As usual the DataGrid should be able to bind its ItemsSource to a List / ObservableCollection. The new part is that it should maintain another bindable list, the SelectedItemsList. Each item added to this list should immediately be selected in the DataGrid.
I found this solution on Stackoverflow: There the DataGrid is extended to hold a Property / DependencyProperty for the SelectedItemsList:
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsList = this.SelectedItems;
}
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register("SelectedItemsList",
typeof(IList),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
In the View/XAML this property is bound to the ViewModel:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ucc:CustomDataGrid Grid.Row="0"
ItemsSource="{Binding DataGridItems}"
SelectionMode="Extended"
AlternatingRowBackground="Beige"
SelectionUnit="FullRow"
IsReadOnly="True"
SelectedItemsList="{Binding DataGridSelectedItems,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Row="1"
Margin="5"
HorizontalAlignment="Center"
Content="Select some rows"
Command="{Binding CmdSelectSomeRows}"/>
</Grid>
The ViewModel also implements the command CmdSelectSomeRows which selects some rows for testing. The ViewModel of the test application looks like this:
public class CustomDataGridViewModel : ObservableObject
{
public IList DataGridSelectedItems
{
get { return dataGridSelectedItems; }
set
{
dataGridSelectedItems = value;
OnPropertyChanged(nameof(DataGridSelectedItems));
}
}
public ICommand CmdSelectSomeRows { get; }
public ObservableCollection<ExamplePersonModel> DataGridItems { get; private set; }
public CustomDataGridViewModel()
{
// Create some example items
DataGridItems = new ObservableCollection<ExamplePersonModel>();
for (int i = 0; i < 10; i++)
{
DataGridItems.Add(new ExamplePersonModel
{
Name = $"Test {i}",
Age = i * 22
});
}
CmdSelectSomeRows = new RelayCommand(() =>
{
if (DataGridSelectedItems == null)
{
DataGridSelectedItems = new ObservableCollection<ExamplePersonModel>();
}
else
{
DataGridSelectedItems.Clear();
}
DataGridSelectedItems.Add(DataGridItems[0]);
DataGridSelectedItems.Add(DataGridItems[1]);
DataGridSelectedItems.Add(DataGridItems[4]);
DataGridSelectedItems.Add(DataGridItems[6]);
}, () => true);
}
private IList dataGridSelectedItems = new ArrayList();
}
This works, but only partially: After application start when items are added to the SelectedItemsList from ViewModel, they are not displayed as selected rows in the DataGrid. To get it to work I must first select some rows with the mouse. When I then add items to the SelectedItemsList from ViewModel these are displayed selected – as I want it.
How can I achieve this without having to first select some rows with the mouse?
You should subscribe to the Loaded event in your CustomDataGrid and initialize the SelectedItems of the Grid (since you never entered the SelectionChangedEvent, there is no link between the SelectedItemsList and the SelectedItems of your DataGrid.
private bool isSelectionInitialization = false;
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
this.isSelectionInitialization = true;
foreach (var item in this.SelectedItemsList)
{
this.SelectedItems.Clear();
this.SelectedItems.Add(item);
}
this.isSelectionInitialization = false;
}
and the SelectionChanged event handler has to be modified like this:
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!this.isSelectionInitialization)
{
this.SelectedItemsList = this.SelectedItems;
}
else
{
//Initialization from the ViewModel
}
}
Note that while this will fix your problem, this won't be a true synchronization as it will only copy the items from the ViewModel at the beginning.
If you need to change the items in the ViewModel at a later time and have it reflected in the selection let me know and I will edit my answer.
Edit: Solution to have a "true" synchronization
I created a class inheriting from DataGrid like you did.
You will need to add the using
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
this.Loaded += CustomDataGrid_Loaded;
}
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
//Can't do it in the constructor as the bound values won't be initialized
//If it is expected for the bound collection to be null initially, you could subscribe to the change of the
//dependency in order to subscribe to the collectionChanged event on the first non null value
this.SelectedItemsList.CollectionChanged += SelectedItemsList_CollectionChanged;
//We call the update in case we have already some items in the VM collection
this.UpdateUIWithSelectedItemsFromVm();
if(this.SelectedItems.Count != 0)
{
//Otherwise the items won't be as visible unless you change the style (this part is not required)
this.Focus();
}
else
{
//No focus
}
}
private void SelectedItemsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.UpdateUIWithSelectedItemsFromVm();
}
private void UpdateUIWithSelectedItemsFromVm()
{
if (!this.isSelectionChangeFromUI)
{
this.isSelectionChangeFromViewModel = true;
this.SelectedItems.Clear();
if (this.SelectedItemsList == null)
{
//Nothing to do, we just cleared all the selections
}
else
{
if (this.SelectedItemsList is IList iListFromVM)
foreach (var item in iListFromVM)
{
this.SelectedItems.Add(item);
}
}
this.isSelectionChangeFromViewModel = false;
}
else
{
//Nothing to do, the change is coming from the SelectionChanged event
}
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//If your collection allow suspension of notifications, it would be a good idea to add a check here in order to use it
if(!this.isSelectionChangeFromViewModel)
{
this.isSelectionChangeFromUI = true;
if (this.SelectedItemsList is IList iListFromVM)
{
iListFromVM.Clear();
foreach (var item in SelectedItems)
{
iListFromVM.Add(item);
}
}
else
{
throw new InvalidOperationException("The bound collection must inherit from IList");
}
this.isSelectionChangeFromUI = false;
}
else
{
//Nothing to do, the change is comming from the bound collection so no need to update it
}
}
private bool isSelectionChangeFromUI = false;
private bool isSelectionChangeFromViewModel = false;
public INotifyCollectionChanged SelectedItemsList
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register(nameof(SelectedItemsList),
typeof(INotifyCollectionChanged),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
You will have to initialize the DataGridSelectedItems earlier or you there will be a null exception when trying to subscribe to the collectionChanged event.
/// <summary>
/// I removed the notify property changed from your example as it probably isn't necessary unless you really intended to create a new Collection at some point instead of just clearing the items
/// (In this case you will have to adapt the code for the synchronization of CustomDataGrid so that it subscribe to the collectionChanged event of the new collection)
/// </summary>
public ObservableCollection<ExamplePersonModel> DataGridSelectedItems { get; set; } = new ObservableCollection<ExamplePersonModel>();
I didn't try all the edge cases but this should give you a good start and I added some directions as to how to improve it. Let me know if some parts of the code aren't clear and I will try to add some comments.
I have dynamic listbox contains textbox to display list items and so I can edit listbox item. My application setting file contains string collection which I want to bind for that listbox. I also want to update that setting files on every change of listbox item, I created class which implements INotifyProprtyChanged. I have converted string collection from settings file into observable collection of custom type which has string property. I bind textbox to property of that custom class and update source property on property change. I want to update observable collection as well. and that updates my app setting file as well. Please help me on this. Any help would be really appreciated. My code:
public class WindowViewModel : INotifyPropertyChanged
{
private ObservableCollection<UrlModel> customcollection;
public ObservableCollection<UrlModel> CustomCollection
{
get { return customcollection; }
set
{
customcollection = value;
NotifyPropertyChanged("CustomCollection");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
public WindowViewModel()
{
List<string> customlist = Properties.Settings.Default.CustomList.Cast<string>().ToList();
List<UrlModel> urllist = new List<UrlModel>();
urllist = customlist.Select(item => new UrlModel() { urlString = item }).ToList();
CustomCollection = new ObservableCollection<UrlModel>(urllist);
}
}
public class UrlModel : INotifyPropertyChanged
{
private string url;
public string urlString
{
get { return url; }
set
{
url = value;
NotifyPropertyChanged("urlString");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ViewModel = new WindowViewModel();
ListTwo.ItemsSource = ViewModel.CustomCollection;
}
private WindowViewModel viewModel;
public WindowViewModel ViewModel
{
get { return viewModel; }
set{
viewModel = value;
DataContext = value;
}
}
}
}
Add property changed event handler for each url when inserting into ObservableCollection
public WindowViewModel()
{
List<string> customlist = Properties.Settings.Default.CustomList.Cast<string>().ToList();
List<UrlModel> urllist = new List<UrlModel>();
urllist = customlist.Select(item => new UrlModel() { urlString = item }).ToList();
CustomCollection = new ObservableCollection<UrlModel>(urllist);
foreach(var model in CustomCollection)
{
model.PropertyChaged += SettingsUpdater; //Settings update fucntion
}
}
A naive implementation of SettingsUpdater would just update the whole list of urls in settings whenever one of them changes.
I believe you are using data template to make your listbox editable. If that is the case, while binding the text, include Text="{Binding urlString,UpdateSourceTrigger=PropertyChanged}" in the Xaml code.
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Name="EditableText" Text="{Binding urlString,UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</ListBox.ItemTemplate>
I'm building a WPF application and my UI consists of combobox and about a dozen other UI controls. I have a single business object class that contains a dozen or so properties and implements INotifyPropertyChanged.
Here's a snippet of my business object:
public class MyBusinessObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
private int _idNumber;
public int IdNumber
{
get { return _idNumber; }
set
{
if (_idNumber == value)
{
return;
}
_idNumber = value;
OnPropertyChanged(new PropertyChangedEventArgs("IdNumber"));
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value)
{
return;
}
_name = value;
OnPropertyChanged(new PropertyChangedEventArgs("Name"));
}
}
private int _age;
public int Age
{
get { return _age; }
set
{
if (_age == value)
{
return;
}
_age = value;
OnPropertyChanged(new PropertyChangedEventArgs("Age"));
}
}
private string _address;
public string Address
{
get { return _address; }
set
{
if (_address == value)
{
return;
}
_address = value;
OnPropertyChanged(new PropertyChangedEventArgs("Address"));
}
}
private bool _employed;
public bool Employed
{
get { return _employed; }
set
{
if (_employed == value)
{
return;
}
_employed = value;
OnPropertyChanged(new PropertyChangedEventArgs("Employed"));
}
}
public MyBusinessObject(int idNumber)
{
this.IdNumber = idNumber;
// set default values here
}
}
As you might expect, the various UI controls will be bound to the properties of my business object. However, I need to create an array or list of business objects (10 of them to be specific) and bind my combobox to the IdNumber property. So my user will select the object that they want from the combobox and then the other UI controls should update to display the values for each of their bound properties for the selected object.
Right now, I just have one instance of my business object declared in my code behind like this:
public partial class MainWindow : Window
{
// this will be replaced with an array/list of business objects
MyBusinessObject myObject = new MyBusinessObject(1234); // 1234 = IdNumber
public MainWindow()
{
InitializeComponent();
this.DataContext = myObject;
}
}
And currently, my combobox is defined like this:
<ComboBox x:Name="selectedObjectComboBox" IsEditable="False"/>
Once I implement my array/list of business objects, can anyone tell me how I would bind the combobox to the array so that it will display the IdNumber for each object? Also, what, if anything, will I need to do to get the other bound controls to reflect the values of the selected object when the user changes their selection in the combobox?
You need to bind your ComboBox to your list, then use the DisplayMemberPath to specify which member you want displayed:
<ComboBox ItemsSource={Binding yourList} DisplayMemberPath="IdNumber"/>
If you want your other controls to update off of this value, you might want to consider making a SelectedItem property on your view model and bind the selected item to this. Then your other controls can bind to this.
EDIT
That can be achieved by doing something like:
<ComboBox ItemsSource={Binding yourList} SelectedItem={Binding SelectedBusinessObject} DisplayMemberPath="IdNumber"/>
If you don't want to make a backing field, you could do something like..
<ComboBox ItemsSource={Binding yourList} x:Name="BusinessComboBox" DisplayMemberPath="IdNumber"/>
<MyControl Item={Binding SelectedItem, ElementName=BusinessComboBox />
I need to update the list of downloads when the progress has been changed.
XAML:
<ListView ItemsSource="{Binding UiList}" x:Name="MyListView">
<ListView.View>
<GridView>
<GridViewColumn Header="Title"/>
<GridViewColumn Header="Progress"/>
</GridView>
</ListView.View>
</ListView>
ViewModel that creates an instance of Logic class and updates the content of ListView:
class MainWindowViewModel : ViewModelBase
{
private readonly Logic _logic;
public List<Model> UiList { get; set; }
public MainWindowViewModel()
{
_logic = new Logic();
_logic.Update += LogicUpdate;
Start = new RelayCommand(() =>
{
var worker = new BackgroundWorker();
worker.DoWork += (sender, args) => _logic.Start();
worker.RunWorkerAsync();
});
}
void LogicUpdate(object sender, EventArgs e)
{
UiList = _logic.List;
RaisePropertyChanged("UiList");
}
public ICommand Start { get; set; }
}
Logic:
public class Logic
{
readonly List<Model> _list = new List<Model>();
public event EventHandler Update;
public List<Model> List
{
get { return _list; }
}
public void Start()
{
for (int i = 0; i < 100; i++)
{
_list.Clear();
_list.Add(new Model{Progress = i, Title = "title1"});
_list.Add(new Model { Progress = i, Title = "title2" });
var time = DateTime.Now.AddSeconds(2);
while (time > DateTime.Now)
{ }
Update(this, EventArgs.Empty);
}
}
}
The code above would not update UI. I know two way how to fix this:
In xaml codebehind call: Application.Current.Dispatcher.Invoke(new Action(() => MyListView.Items.Refresh()));
In ViewModel change List<> to ICollectionView and use Application.Current.Dispatcher.Invoke(new Action(() => UiList.Refresh())); after the list has been updated.
Both ways cause the problem: the ListView blinks and Popup that should be open on user demand always closes after each "refresh":
<Popup Name="Menu" StaysOpen="False">
I can replace the popup with another control or panel, but I need its possibility to be out of main window's border (like on screen). But I believe that WPF has another way to update the content of ListView (without blinks).
PS: Sorry for long question, but I do not know how to describe it more briefly...
I think the reason this line doesn't work:
RaisePropertyChanged("UiList");
Is because you haven't actually changed the list. You cleared it and repopulated it, but it's still the reference to the same list. I'd be interested to see what happens if, instead of clearing your list and repopulating, you actually created a new list. I think that should update your ListView as you expected. Whether or not it has an effect on your popup, I don't know.
I've found the answer here: How do I update an existing element of an ObservableCollection?
ObservableCollection is a partial solution. ObservableCollection rises CollectionChanged event only when collection changes (items added, removed, etc.) To support updates of existent items, each object inside the collection (Model class in my case) must implement the INotifyPropertyChanged interface.
// I used this parent (ViewModelBase) for quick testing because it implements INotifyPropertyChanged
public class Model : ViewModelBase
{
private int _progress;
public int Progress
{
get { return _progress; }
set
{
_progress = value;
RaisePropertyChanged("Progress");
}
}
public string Title { get; set; }
}