I'm aware that there are several posts about how to implement datagrid mutliselection.
I've found the open issue on Github (https://github.com/dotnet/wpf/issues/3140).
The user ice1e0 mentioned a solution with a CustomDataGrid class.
public class CustomDataGrid : DataGrid
{
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(CustomDataGrid), new PropertyMetadata(default(IList), OnSelectedItemsPropertyChanged));
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
SetValue(SelectedItemsProperty, base.SelectedItems);
}
public new IList SelectedItems
{
get => (IList)GetValue(SelectedItemsProperty);
set => throw new Exception("This property is read-only. To bind to it you must use 'Mode=OneWayToSource'.");
}
private static void OnSelectedItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((CustomDataGrid)d).OnSelectedItemsChanged((IList)e.OldValue, (IList)e.NewValue);
}
protected virtual void OnSelectedItemsChanged(IList oldSelectedItems, IList newSelectedItems)
{
}
}
So, I tried to implement it, so I copied the class.
And my xaml looks something like that:
<Project1:CustomDataGrid ItemsSource="{Binding Data}" SelectionMode="Extended" SelectionUnit="FullRow" IsReadOnly="True" SelectedItems="{Binding SelectedComputers, Mode=OneWayToSource}" />
and my ViewModel:
[ObservableProperty]
ObservableCollection<string> _selectedComputers;
I use CommunityToolkit.Mvvm (v8.0).
It compiles but SelectedComputers is null, after I select one or multiple items.
UPDATE:
The issue seems that
void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsList = this.SelectedItems;
}
doesn't work. SelectedItemsList is always null. The problem is mentioned in the other thread: Select multiple items from a DataGrid in an MVVM WPF project
The suggested solution by M463:
this.SelectedItemsList = this.SelectedItems; did not work for me, as SelectedItemsList was always set to null. However, changing the code to foreach (var item in this.SelectedItems) { this.SelectedItemsList.Add(item); } did the trick. Please note, that this requires you to call this.SelectedItemsList.Clear(); in advance, so the items of SelectedItemsList will not get duplicated.
When I try the solution, after selected a row the application crashes with the error:
System.ArgumentException: 'The value "System.Data.DataRowView" is not of type "System.String" and cannot be used in this generic collection.
Parameter name: value'
You cannot mix types.
If you Data is a DataView, SelectedItems cannot be bound to an ObservableCollection<string> because you cannot add a (selected) DataRowView to an ObservableCollection<string>.
Also, you cannot set an ObservableCollection<string> property to anything else than an ObservableCollection<string>. That's why the value in the setter of the source property comes in as null when you try to set it to an IList using SetValue.
If you make SelectedComputers an IList, you should see it being set from the control:
[ObservableProperty]
IList _selectedComputers;
The other option is obviously to change the type of the dependency property.
Related
How can I select several rows into a ListView from the code with MVVM Pattern ?
The ListView that I use was made by a teammate who is no more there
public static readonly DependencyProperty SelectedItemsListProperty = DependencyProperty.Register("SelectedItemsList" , typeof(IList) , typeof(SrListView) , new PropertyMetadata(null));
(...)
public IList SelectedItemsList
{
get
{
return ( IList )GetValue(SelectedItemsListProperty);
}
set
{
SetValue(SelectedItemsListProperty , value);
}
}
(...)
private void SrListView_SelectionChanged(object sender , SelectionChangedEventArgs e)
{
SelectedItemsList = SelectedItems;
}
I use this listview like this :
<CustomListView SelectionMode="Extended"
ItemsSource="{Binding ocPackages}"
SelectedItem="{Binding objSelectedPackage}"
SelectedItemsList="{Binding ilSelectedPackages, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</CustomListView>
private IList _ilSelectedPackages; // = new ArrayList()
public IList ilSelectedPackages
{
get
{
return _ilSelectedPackages;
}
set
{
_ilSelectedPackages = value;
OnPropertyChanged(nameof(ilSelectedPackages));
}
}
ilSelectedPackages.Clear();
ilSelectedPackages.Add(objDTO_PackageToSelect);
I try to clear and then fill ilSelectedPackages but this has no effect on the selection of the ListView :(
I found this topic Managing multiple selections with MVVM but I'm not able to solve my problem with it :(
Edit 1 : The "Multiselect ListBox" topic doesn't help me to solve my problem because it is not implemented in the ListViews by default, in my question I explain that it's a homemade ListView and how "SelectedItemsList" was added to the default ListView.
Edit 2 : I tried to modify the homemade ListView component by a "BindableTwoWay" behaviour without success after watching this answer https://stackoverflow.com/a/51254960/10617386 :
public static readonly DependencyProperty SelectedItemsListProperty = DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(SrListView), new FrameworkPropertyMetadata(default(IList),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsListChanged));
(...)
private static void OnSelectedItemsListChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is SrListView ListView)
ListView.SetSelectedItems(ListView.SelectedItemsList);
}
Thanks per advance for your help
I finally found the solution, the problem was not in the homemade component as I was first thinking (I was not searching in the right area) but simply when I was selecting the objects with :
ilSelectedPackages.Add(objDTO_PackageToSelect);
objDTO_PackageToSelect was a copy of an object and so was not comming from ocPackages the ObservableCollection that was filling the ListView.
Conclusion : We must select the exact objects of the Binded observable collection.
DTO_Package objPackInOC = ocPackages.Where(Pack => Pack.sGuid == objDTO_PackageToSelect.sGuid).FirstOrDefault();
if(objPackInOC != null)
ilSelectedPackages.Add(objPackInOC);
I have a simple list of strings which I want to be displayed in a listbox depending on if a checkbox is checked when a button is pressed. I have this logic in my button listener:
private void fileSavePerms_Click(object sender, RoutedEventArgs e)
{
foreach (CheckBox checkbox in checkboxList)
{
if (checkbox.IsChecked == true && !permissionList.Contains(checkbox.Name))
{
permissionList.Add(checkbox.Name);
}
else if (checkbox.IsChecked == false && permissionList.Contains(checkbox.Name))
{
permissionList.Remove(checkbox.Name);
}
}
permListBox.ItemsSource = permissionList;
}
As far as I know, this is how you can do a very simple data-bind on button click. However the listbox updates for the first time as intended, but then will update with incorrect contents of the list I am trying to populate the box with. I can see no discernible pattern with the output.
Furthermore, after a while (a few button clicks), I will catch an exception saying "an ItemsControl is inconsistent with its items source".
Am I setting up my binding incorrectly or assigning the ItemsControl at the incorrect time?
Update:
The XAML for the list box:
<ListBox x:Name="permListBox" ItemsSource="{Binding permissionList}" HorizontalAlignment="Left" Height="36" Margin="28,512,0,0" VerticalAlignment="Top" Width="442"/>
First of all you can bind only properties to a control. A field cannot be bound. So permissionList must be a property of the DataContext object you set to your Window.DataContext property.
If this is correctly set then you can create a new List<string> every time and then assign it to the property bound to the control. You do not have to assign it to the ItemsSource property of the control
Let's say your window's data context is set to the window itself.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
public List<string> PermissionList
{
get { return (List<string>)GetValue(PermissionListProperty); }
set { SetValue(PermissionListProperty, value); }
}
public static readonly DependencyProperty PermissionListProperty =
DependencyProperty.Register(
"PermissionList",
typeof(List<string>),
typeof(MainWindow),
new PropertyMetadata(new List<string>())
);
private void fileSavePerms_Click(object sender, RoutedEventArgs e)
{
// You create a new instance of List<string>
var newPermissionList = new List<string>();
// your foreach statement that fills this newPermissionList
// ...
// At the end you simply replace the property value with this new list
PermissionList = newPermissionList;
}
}
In the XAML file you will have this:
<ListBox
ItemsSource="{Binding PermissionList}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="28,512,0,0"
Height="36"
Width="442"/>
Of course this solution can be improved.
You may use System.Collections.ObjectModel.ObservableCollection<string> type so that you no longer have to create a new instance of List<string> every time but you can clear the list and add the new items in your foreach statement.
You may use a ViewModel class (e.g. MainViewModel) that has this permission list and also implements the INotifyPropertyChanged interface and then you set an instance of this class to your WPF window's DataContext property.
My problem is that the ComboBox is not displaying the value stored in its bound list.
Here is what I'm doing:
WPF:
<ComboBox ItemsSource="{Binding Devices}"
DropDownOpened="deviceSelector_DropDownOpened"/>
Note that my Window's DataContext is {Binding RelativeSource={RelativeSource Self}}.
C# code-behind:
public List<String> Devices { get; set; }
private void deviceSelector_DropDownOpened(object sender, EventArgs e)
{
// the actual population of the list is occuring in another method
// as a result of a database query. I've confirmed that this query is
// working properly and Devices is being populated.
var dev = new List<String>();
dev.Add("Device 1");
dev.Add("Device 2");
Devices = dev;
}
I have tried doing this with an ObservableCollection instead of a List, and I've also tried using a PropertyChangedEventHandler. Neither of these approaches have worked for me.
Any idea why my items aren't being displayed when I click the dropdown?
Since you're doing this in code behind anyway, why not set the ComboBox.ItemsSource directly.
Now, I am not going to say this is the way it should be done in WPF (I would prefer the view's data to be loaded in a ViewModel), but it will solve your issue.
The reason why this isn't working is because your property doesn't inform the binding system when it changes. I know you said you tried it with PropertyChangedEventHandler, but that won't work unless your View looks like this:
public class MyView : UserControl, INotifyPropertyChanged
{
private List<String> devices;
public event PropertyChangedEventHandler PropertyChanged;
public List<String> Devices
{
get { return devices; }
set
{
devices = value;
// add appropriate event raising pattern
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Devices"));
}
}
...
}
Likewise, using an ObservableCollection would only work like this:
private readonly ObservableCollection<string> devices = new ObservableCollection<string>();
public IEnumerable<string> Devices { get { return devices; } }
private void deviceSelector_DropDownOpened(object sender, EventArgs e)
{
devices.Clear();
devices.Add("Device 1");
devices.Add("Device 2");
}
Either method should populate the ComboBox, and in a quick test I just ran, it worked.
Edit to add DependencyProperty method
One last way you can do this is with a DependencyProperty (as your View is a DependencyObject:
public class MyView : UserControl
{
public static readonly DependencyProperty DevicesProperty = DependencyProperty.Register(
"Devices",
typeof(List<string>),
typeof(MainWindow),
new FrameworkPropertyMetadata(null));
public List<string> Devices
{
get { return (List<string>)GetValue(DevicesProperty); }
set { SetValue(DevicesProperty, value); }
}
...
}
The following change (suggested by Abe Heidebrecht) fixed the problem, but I don't know why. Anyone willing to lend an explanation?
WPF:
<ComboBox DropDownOpened="deviceSelector_DropDownOpened"
Name="deviceSelector"/>
C# code-behind:
private void deviceSelector_DropDownOpened(object sender, EventArgs e)
{
var dev = new List<String>();
dev.Add("Device 1");
dev.Add("Device 2");
deviceSelector.ItemsSource = dev;
}
Unless I'm missing something here:
Try firing OnPropertyChanged when devices gets updated for the devices property, this should fix this. I have occasionally also had to set the mode:
ItemsSource="{Binding Devices, Mode=TwoWay}"
On some controls.
Setting the itemssource on the control directly tells the control to use new items directly, without using the binding hooked up in the xaml. Updating the Devices property on the datacontext does not tell the combobox that the Devices property has changed so it won't update. The way to inform the combobox of the change is to fire OnPropertyChanged for the devices property when it gets changed.
I have a ComboBox with few static values.
<ComboBox Name="cmbBoxField" Grid.Column="4" Grid.Row="2" Style="{StaticResource comboBoxStyleFixedWidth}" ItemsSource="{Binding}" ></ComboBox>
MVVMModle1.cmbBoxField.Items.Add(new CustomComboBoxItem("Text Box", "0"));
MVVMModle1.cmbBoxFieldType.Items.Add(new CustomComboBoxItem("Pick List", "1"));
MVVMModle1.cmbBoxFieldType.Items.Add(new CustomComboBoxItem("Check Box", "2"));
MVVMModle1.cmbBoxFieldType.Items.Add(new CustomComboBoxItem("Radio Button", "3"));
When I am saving the data in Database table it is getting saved.
((CustomComboBoxItem)(MVVMModle1.cmbBoxField.SelectedValue)).Value.ToString();
Now when I am trying to Edit my form and binding the value again to combobox it is not showing the value.
MVVMModle1.cmbBoxField.SelectedValue = dtDataList.Rows[0]["ControlList"].ToString().Trim();
Someone please help me in this. How to bind selected value to the combobox?
There are quite a few problems with your code here:
You are setting the ItemsControl.ItemsSource property to the default binding (bind to the current data context), which is incorrect unless the DataContext is any type that implements IEnumerable, which it probably isn't.
If this is correct because the DataContext is, for example, an ObservableCollection<T>, then you still have an issue because you are adding items statically to the ComboBox instead of whatever the ItemsSource is.
Also, the type of items you are adding are CustomComboBoxItem, which I'm going to assume inherits from ComboBoxItem. Either way, you can't say the SelectedValue is some string since the values in the ComboBox are not strings.
You should really not have a collection of CustomComboBoxItem's, but instead a custom class that is in itself it's own ViewModel.
Now that that's been said, here is a suggested solution to your problem:
<ComboBox ItemsSource="{Binding Path=MyCollection}"
SelectedValue="{Binding Path=MySelectedString}"
SelectedValuePath="StringProp" />
public class CustomComboBoxItem : ComboBoxItem
{
// Not sure what the property name is...
public string StringProp { get; set; }
...
}
// I'm assuming you don't have a separate ViewModel class and you're using
// the actual window/page as your ViewModel (which you shouldn't do...)
public class MyWPFWindow : Window, INotifyPropertyChanged
{
public MyWPFWindow()
{
MyCollection = new ObservableCollection<CustomComboBoxItem>();
// Add values somewhere in code, doesn't have to be here...
MyCollection.Add(new CustomComboBoxItem("Text Box", "0"));
etc ...
InitializeComponent();
}
public ObservableCollection<CustomComboBoxItem> MyCollection
{
get;
private set;
}
private string _mySelectedString;
public string MySelectedString
{
get { return _mySelectedString; }
set
{
if (String.Equals(value, _mySelectedString)) return;
_mySelectedString = value;
RaisePropertyChanged("MySelectedString");
}
}
public void GetStringFromDb()
{
// ...
MySelectedString = dtDataList.Rows[0]["ControlList"].ToString().Trim();
}
}
You could alternatively not implement INotifyPropertyChanged and use a DependencyProperty for your MySelectedString property, but using INPC is the preferred way. Anyways, that should give you enough information to know which direction to head in...
TL;DR;
Take advantage of binding to an ObservableCollection<T> (create a property for this).
Add your items (CustomComboBoxItems) to the ObservableCollection<T>.
Bind the ItemsSource to the new collection property you created.
Bind the SelectedValue to some string property you create (take advantage of INPC).
Set the SelectedValuePath to the path of the string property name of your CustomComboBoxItem.
Can you use cmbBoxField.DataBoundItem()? If not target the source from the selected value, i.e. Get the ID then query the source again to get the data.
(CustomComboBoxItem)MVVMModle1.cmbBoxField.DataBoundItem();
When you bind a datasource it is simpler to do it like this:
private List GetItems(){
List<CustomComboBoxItem> items = new List<CustomComboBoxItem>();
items.Add(new CustomComboBoxItem() {Prop1 = "Text Box", Prop2 = "0"});
//...and so on
return items;
}
Then in your main code:
List<CustomComboBoxItem> items = this.GetItems();
MVVMModle1.cmbBoxField.DisplayMember = Prop1;
MVVMModle1.cmbBoxField.ValueMember = Prop2;
MVVMModle1.cmbBoxField.DataSource = items;
This will then allow your selected value to work, either select by index, value or text
var selected = dtDataList.Rows[0]["ControlList"].ToString().Trim();
MVVMModle1.cmbBoxField.SelectedValue = selected;
Here are the relevant parts of the XAML file:
xmlns:local="clr-namespace:BindingTest"
<ListBox x:Name="myList"
ItemsSource="{Binding Source={x:Static local:MyClass.Dic},
Path=Keys,
Mode=OneWay,
UpdateSourceTrigger=Explicit}">
</ListBox>
MyClass is a public static class and Dic is a static public property, a Dictionary.
At a certain point I add items to the Dictionary and would like the ListBox to reflect the changes.
This is the code I thought about using but it doesn't work:
BindingExpression binding;
binding = myList.GetBindingExpression(ListBox.ItemsSourceProperty);
binding.UpdateTarget();
This code instead works:
myList.ItemsSource = null;
myList.ItemsSource = MyClass.dic.Keys;
I would prefer to use UpdateTarget, but I can't get it to work.
What am I doing wrong?
Binding of items is handled rather differently than standard binding of DependencyPropertys in WPF (specifically, by ItemsControls).
I think you want something like the following:
var itemsView = CollectionViewSource.GetDefaultView(myListBox.ItemsSource);
itemsView.Refresh()
It is in fact the ICollectionView object that you want to refresh. This effectively is the object that manages the collection binding for you. See the MSDN page for more info.
Expanding on the previous answer by Noldorin, you can hook to the PropertyChanged event to make this happen automatically anytime your model is updated:
public partial class YourView {
public YourView () {
InitializeComponent();
ViewModel=YourDataObject;
ViewModel.PropertyChanged+=OnUpdateYourDataObject;
}
public YourDataObject? ViewModel {
get => DataContext as YourDataObject;
set => DataContext=value;
}
public void OnUpdateYourDataObject(object? sender, PropertyChangedEventArgs? e) =>
CollectionViewSource.GetDefaultView(YourListBox.ItemsSource).Refresh();
}
Replace YourDataObject and YourListBox with the correct values. This will automatically update your view whenever a property on your object changes. I'm not sure why this isn't handled automatically but I couldn't figure out the setting to make that work.