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. !!!
I have a WPF ComboBox bound to a list of a class which contains an enum.
This all works fine, my question is at the end of this post, first the code:
Here is the class:
public class FILTER_TEST
{
public FilterType Filter { get; private set; }
public string Description { get; private set; }
public static List<FILTER_TEST> CreateFilters()
{
var list = new List<FILTER_TEST>();
list.Add(new FILTER_TEST() { Filter = FilterType.CheckNone, Description = "Uncheck all" });
list.Add(new FILTER_TEST() { Filter = FilterType.CheckAll, Description = "Check all" });
list.Add(new FILTER_TEST() { Filter = FilterType.CheckCustom, Description = "Custom check" });
return list;
}
}
Here is the enum FilterType:
public enum FilterType
{
CheckNone,
CheckAll,
CheckCustom
}
In my view model I have the following:
public List<FILTER_TEST> FilterNames { get { return FILTER_TEST.CreateFilters(); } }
public FILTER_TEST SelectedFilter
{
get { return selectedFilter; }
set
{
if (value != selectedFilter)
{
selectedFilter = value;
OnPropertyChanged("SelectedFilter");
}
}
}
Also in the view model, I set the SelectedItem of the ComboBox as follows:
SelectedFilter = FilterNames.Where(x => x.Filter == FilterType.CheckNone).FirstOrDefault();
Here is the xaml putting it all together:
<ComboBox DisplayMemberPath="Description" ItemsSource="{Binding FilterNames}"
SelectedItem="{Binding SelectedFilter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="True"/>
My problem is that although the changing of the SelectionItem works, the actual value displayed in the ComboBox doesn’t change.
The initial SelectedItem is “Uncheck all” as, when the window has been loaded, none of the corresponding CheckBox controls (bound to another class which contains a Boolean property) have been checked. What I would like is that when a CheckBox has been checked, then the SelectedItem changes to “Custom check”.
This does indeed change the value of the SelectedItem:
SelectedFilter = FilterNames.Where(x => x.Filter == FilterType.CheckCustom).FirstOrDefault();
But the text shown in the ComboBox is still “Uncheck all”.
Does anyone have an idea as to what I am missing? I am forced to use the 4.0 framework, I don’t know if this is relevant.
I've seen the hint to overwrite Equals() of the type in use as this:
public override bool Equals(object o)
{
if (o is FILTER_TEST)
{
var other = o as FILTER_TEST;
return this.Description == other.Description && this.Filter == other.Filter;
}
else
return false;
}
Now that makes your sample work. Let me come back for a reference on the why.
My WPF application generates sets of data which may have a different number of columns each time. Included in the output is a description of each column that will be used to apply formatting. A simplified version of the output might be something like:
class Data
{
IList<ColumnDescription> ColumnDescriptions { get; set; }
string[][] Rows { get; set; }
}
This class is set as the DataContext on a WPF DataGrid but I actually create the columns programmatically:
for (int i = 0; i < data.ColumnDescriptions.Count; i++)
{
dataGrid.Columns.Add(new DataGridTextColumn
{
Header = data.ColumnDescriptions[i].Name,
Binding = new Binding(string.Format("[{0}]", i))
});
}
Is there any way to replace this code with data bindings in the XAML file instead?
Here's a workaround for Binding Columns in the DataGrid. Since the Columns property is ReadOnly, like everyone noticed, I made an Attached Property called BindableColumns which updates the Columns in the DataGrid everytime the collection changes through the CollectionChanged event.
If we have this Collection of DataGridColumn's
public ObservableCollection<DataGridColumn> ColumnCollection
{
get;
private set;
}
Then we can bind BindableColumns to the ColumnCollection like this
<DataGrid Name="dataGrid"
local:DataGridColumnsBehavior.BindableColumns="{Binding ColumnCollection}"
AutoGenerateColumns="False"
...>
The Attached Property BindableColumns
public class DataGridColumnsBehavior
{
public static readonly DependencyProperty BindableColumnsProperty =
DependencyProperty.RegisterAttached("BindableColumns",
typeof(ObservableCollection<DataGridColumn>),
typeof(DataGridColumnsBehavior),
new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = source as DataGrid;
ObservableCollection<DataGridColumn> columns = e.NewValue as ObservableCollection<DataGridColumn>;
dataGrid.Columns.Clear();
if (columns == null)
{
return;
}
foreach (DataGridColumn column in columns)
{
dataGrid.Columns.Add(column);
}
columns.CollectionChanged += (sender, e2) =>
{
NotifyCollectionChangedEventArgs ne = e2 as NotifyCollectionChangedEventArgs;
if (ne.Action == NotifyCollectionChangedAction.Reset)
{
dataGrid.Columns.Clear();
foreach (DataGridColumn column in ne.NewItems)
{
dataGrid.Columns.Add(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Add)
{
foreach (DataGridColumn column in ne.NewItems)
{
dataGrid.Columns.Add(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Move)
{
dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
}
else if (ne.Action == NotifyCollectionChangedAction.Remove)
{
foreach (DataGridColumn column in ne.OldItems)
{
dataGrid.Columns.Remove(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Replace)
{
dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
}
};
}
public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
{
element.SetValue(BindableColumnsProperty, value);
}
public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
{
return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
}
}
I've continued my research and have not found any reasonable way to do this. The Columns property on the DataGrid isn't something I can bind against, in fact it's read only.
Bryan suggested something might be done with AutoGenerateColumns so I had a look. It uses simple .Net reflection to look at the properties of the objects in ItemsSource and generates a column for each one. Perhaps I could generate a type on the fly with a property for each column but this is getting way off track.
Since this problem is so easily sovled in code I will stick with a simple extension method I call whenever the data context is updated with new columns:
public static void GenerateColumns(this DataGrid dataGrid, IEnumerable<ColumnSchema> columns)
{
dataGrid.Columns.Clear();
int index = 0;
foreach (var column in columns)
{
dataGrid.Columns.Add(new DataGridTextColumn
{
Header = column.Name,
Binding = new Binding(string.Format("[{0}]", index++))
});
}
}
// E.g. myGrid.GenerateColumns(schema);
I have found a blog article by Deborah Kurata with a nice trick how to show variable number of columns in a DataGrid:
Populating a DataGrid with Dynamic Columns in a Silverlight Application using MVVM
Basically, she creates a DataGridTemplateColumn and puts ItemsControl inside that displays multiple columns.
I managed to make it possible to dynamically add a column using just a line of code like this:
MyItemsCollection.AddPropertyDescriptor(
new DynamicPropertyDescriptor<User, int>("Age", x => x.Age));
Regarding to the question, this is not a XAML-based solution (since as mentioned there is no reasonable way to do it), neither it is a solution which would operate directly with DataGrid.Columns. It actually operates with DataGrid bound ItemsSource, which implements ITypedList and as such provides custom methods for PropertyDescriptor retrieval. In one place in code you can define "data rows" and "data columns" for your grid.
If you would have:
IList<string> ColumnNames { get; set; }
//dict.key is column name, dict.value is value
Dictionary<string, string> Rows { get; set; }
you could use for example:
var descriptors= new List<PropertyDescriptor>();
//retrieve column name from preprepared list or retrieve from one of the items in dictionary
foreach(var columnName in ColumnNames)
descriptors.Add(new DynamicPropertyDescriptor<Dictionary, string>(ColumnName, x => x[columnName]))
MyItemsCollection = new DynamicDataGridSource(Rows, descriptors)
and your grid using binding to MyItemsCollection would be populated with corresponding columns. Those columns can be modified (new added or existing removed) at runtime dynamically and grid will automatically refresh it's columns collection.
DynamicPropertyDescriptor mentioned above is just an upgrade to regular PropertyDescriptor and provides strongly-typed columns definition with some additional options. DynamicDataGridSource would otherwise work just fine event with basic PropertyDescriptor.
Made a version of the accepted answer that handles unsubscription.
public class DataGridColumnsBehavior
{
public static readonly DependencyProperty BindableColumnsProperty =
DependencyProperty.RegisterAttached("BindableColumns",
typeof(ObservableCollection<DataGridColumn>),
typeof(DataGridColumnsBehavior),
new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
/// <summary>Collection to store collection change handlers - to be able to unsubscribe later.</summary>
private static readonly Dictionary<DataGrid, NotifyCollectionChangedEventHandler> _handlers;
static DataGridColumnsBehavior()
{
_handlers = new Dictionary<DataGrid, NotifyCollectionChangedEventHandler>();
}
private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = source as DataGrid;
ObservableCollection<DataGridColumn> oldColumns = e.OldValue as ObservableCollection<DataGridColumn>;
if (oldColumns != null)
{
// Remove all columns.
dataGrid.Columns.Clear();
// Unsubscribe from old collection.
NotifyCollectionChangedEventHandler h;
if (_handlers.TryGetValue(dataGrid, out h))
{
oldColumns.CollectionChanged -= h;
_handlers.Remove(dataGrid);
}
}
ObservableCollection<DataGridColumn> newColumns = e.NewValue as ObservableCollection<DataGridColumn>;
dataGrid.Columns.Clear();
if (newColumns != null)
{
// Add columns from this source.
foreach (DataGridColumn column in newColumns)
dataGrid.Columns.Add(column);
// Subscribe to future changes.
NotifyCollectionChangedEventHandler h = (_, ne) => OnCollectionChanged(ne, dataGrid);
_handlers[dataGrid] = h;
newColumns.CollectionChanged += h;
}
}
static void OnCollectionChanged(NotifyCollectionChangedEventArgs ne, DataGrid dataGrid)
{
switch (ne.Action)
{
case NotifyCollectionChangedAction.Reset:
dataGrid.Columns.Clear();
foreach (DataGridColumn column in ne.NewItems)
dataGrid.Columns.Add(column);
break;
case NotifyCollectionChangedAction.Add:
foreach (DataGridColumn column in ne.NewItems)
dataGrid.Columns.Add(column);
break;
case NotifyCollectionChangedAction.Move:
dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Remove:
foreach (DataGridColumn column in ne.OldItems)
dataGrid.Columns.Remove(column);
break;
case NotifyCollectionChangedAction.Replace:
dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
break;
}
}
public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
{
element.SetValue(BindableColumnsProperty, value);
}
public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
{
return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
}
}
You can create a usercontrol with the grid definition and define 'child' controls with varied column definitions in xaml. The parent needs a dependency property for columns and a method for loading the columns:
Parent:
public ObservableCollection<DataGridColumn> gridColumns
{
get
{
return (ObservableCollection<DataGridColumn>)GetValue(ColumnsProperty);
}
set
{
SetValue(ColumnsProperty, value);
}
}
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.Register("gridColumns",
typeof(ObservableCollection<DataGridColumn>),
typeof(parentControl),
new PropertyMetadata(new ObservableCollection<DataGridColumn>()));
public void LoadGrid()
{
if (gridColumns.Count > 0)
myGrid.Columns.Clear();
foreach (DataGridColumn c in gridColumns)
{
myGrid.Columns.Add(c);
}
}
Child Xaml:
<local:parentControl x:Name="deGrid">
<local:parentControl.gridColumns>
<toolkit:DataGridTextColumn Width="Auto" Header="1" Binding="{Binding Path=.}" />
<toolkit:DataGridTextColumn Width="Auto" Header="2" Binding="{Binding Path=.}" />
</local:parentControl.gridColumns>
</local:parentControl>
And finally, the tricky part is finding where to call 'LoadGrid'.
I am struggling with this but got things to work by calling after InitalizeComponent in my window constructor (childGrid is x:name in window.xaml):
childGrid.deGrid.LoadGrid();
Related blog entry
You might be able to do this with AutoGenerateColumns and a DataTemplate. I'm not positive if it would work without a lot of work, you would have to play around with it. Honestly if you have a working solution already I wouldn't make the change just yet unless there's a big reason. The DataGrid control is getting very good but it still needs some work (and I have a lot of learning left to do) to be able to do dynamic tasks like this easily.
There is a sample of the way I do programmatically:
public partial class UserControlWithComboBoxColumnDataGrid : UserControl
{
private Dictionary<int, string> _Dictionary;
private ObservableCollection<MyItem> _MyItems;
public UserControlWithComboBoxColumnDataGrid() {
_Dictionary = new Dictionary<int, string>();
_Dictionary.Add(1,"A");
_Dictionary.Add(2,"B");
_MyItems = new ObservableCollection<MyItem>();
dataGridMyItems.AutoGeneratingColumn += DataGridMyItems_AutoGeneratingColumn;
dataGridMyItems.ItemsSource = _MyItems;
}
private void DataGridMyItems_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
var desc = e.PropertyDescriptor as PropertyDescriptor;
var att = desc.Attributes[typeof(ColumnNameAttribute)] as ColumnNameAttribute;
if (att != null)
{
if (att.Name == "My Combobox Item") {
var comboBoxColumn = new DataGridComboBoxColumn {
DisplayMemberPath = "Value",
SelectedValuePath = "Key",
ItemsSource = _ApprovalTypes,
SelectedValueBinding = new Binding( "Bazinga"),
};
e.Column = comboBoxColumn;
}
}
}
}
public class MyItem {
public string Name{get;set;}
[ColumnName("My Combobox Item")]
public int Bazinga {get;set;}
}
public class ColumnNameAttribute : Attribute
{
public string Name { get; set; }
public ColumnNameAttribute(string name) { Name = name; }
}
I am developing an application using WPF mvvm approach.
I have a requirement where I have to show a list of items in a combo box for selection.
Based on some flag I need to filter out few items from the combo box for selection.
I tried to use two different items sources one with full list and another with filtered list and based on the flag I wanted to change the items source.
This does not seem to be working well. Is there any easy way to apply filters on the existing list based on some flag ?
There are lots of different ways to do this but my personal preference is to use a ListCollectionView as the ItemsSource of the control displaying the filtered list, to set a filter predicate on ListCollectionView.Filter and to call ListCollectionView.Refresh when the filter parameters change.
The example below will filter a list of countries based on their continent.
Code
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
public class FilteringViewModel : INotifyPropertyChanged
{
private ObservableCollection<Country> _countries;
private ContinentViewModel _selectedContinent;
public ListCollectionView CountryView { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<ContinentViewModel> Continents { get; set; }
public FilteringViewModel()
{
_countries =
new ObservableCollection<Country>(
new[]
{
new Country() { Continent = Continent.Africa, DisplayName = "Zimbabwe" },
new Country() { Continent = Continent.Africa, DisplayName = "Egypt" },
new Country() { Continent = Continent.Europe, DisplayName = "United Kingdom" }
});
CountryView = new ListCollectionView(_countries);
CountryView.Filter = o => _selectedContinent == null || ((Country)o).Continent == _selectedContinent.Model;
Continents = new ObservableCollection<ContinentViewModel>(Enum.GetValues(typeof(Continent)).Cast<Continent>().Select(c => new ContinentViewModel { Model = c}));
}
public ContinentViewModel SelectedContinent
{
get
{
return _selectedContinent;
}
set
{
_selectedContinent = value;
OnContinentChanged();
this.OnPropertyChanged("SelectedContinent");
}
}
private void OnContinentChanged()
{
CountryView.Refresh();
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class Country
{
public string DisplayName { get; set; }
public Continent Continent { get; set; }
}
public enum Continent
{
[Description("Africa")]
Africa,
Asia,
Europe,
America
}
public class ContinentViewModel
{
public Continent Model { get; set; }
public string DisplayName
{
get
{
return Enum.GetName(typeof(Continent), Model);
}
}
}
XAML
<StackPanel Orientation="Vertical">
<ComboBox ItemsSource="{Binding Continents}" SelectedItem="{Binding SelectedContinent}" DisplayMemberPath="DisplayName" />
<ListBox ItemsSource="{Binding CountryView}" DisplayMemberPath="DisplayName" />
</StackPanel>
Is there any easy way to apply filters on the existing list based on
some flag ?
Although your question is not clear, but I think you don't need to maintain two list just to get the filter data. You can use simple LINQ to do the filtering. Suppose if you have a ViewModel Property like
public IEnumerable<ComboBoxItem> Data
{
get ;
set ;
}
And you want to filter that based on some bool values then you can write something like
ViewModel.Data.ToList().Where(item => item.Status).ToList()
Status can be the bool based on that you want to filter your data and you can add this bool inside your ComboBoxItem class.
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;
}
}
}