Deselecting ComboBoxItems in MVVM - wpf

I am using a standard wpf/mvvm application where i bind combo boxes to collections on a ViewModel.
I need to be able to de-select an item from the dropdown. Meaning, users should be able to select something, and later decide that they want to un-select it (select none) for it. the problem is that there are no empty elements in my bound collection
my initial thought was simply to insert a new item in the collection which would result having an empty item on top of the collection.
this is a hack though, and it affects all code that uses that collection on the view model.
for example if someone was to write
_myCollection.Frist(o => o.Name == "foo")
this will throw a null reference exception.
possible workaround is:
_myCollection.Where(o => o != null).First(o => o.Name == "foo");
this will work, but no way to ensure any future uses of that collection won't cause any breaks.
what's a good pattern / solution for being able to adding an empty item so the user can de-select. (I am also aware of CollectionView structure, but that seems like a overkill for something so simple)
Update
went with #hbarck suggestion and implemented CompositeCollection (quick proof of concept)
public CompositeCollection MyObjects {
get {
var col = new CompositeCollection();
var cc1 = new CollectionContainer();
cc1.Collection = _actualCollection;
var cc2 = new CollectionContainer();
cc2.Collection = new List<MyObject>() { null }; // PROBLEM
col.Add(cc2);
col.Add(cc1);
return col;
}
}
this code work with existing bindings (including SelectedItem) which is great.
One problem with this is, that if the item is completely null, the SelectedItem setter is never called upon selecting it.
if i modify that one line to this:
cc2.Collection = new List<MyObject>() { new MyObject() }; // PROBLEM
the setter is called, but now my selected item is just a basic initialized class instead of null.. i could add some code in the setter to check/reset, but that's not good.

I think the easiest way would be to use a CompositeCollection. Just append your collection to another collection which only contains the empty item (null or a placeholder object, whatever suites your needs), and make the CompositeCollection the ItemsSource for the ComboBox. This is probably what it is intended for.
Update:
This turns out to be more complicated than I first thought, but actually, I came up with this solution:
<Window x:Class="ComboBoxFallbackValue"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:t="clr-namespace:TestWpfDataBinding"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:w="clr-namespace:System.Windows;assembly=WindowsBase"
Title="ComboBoxFallbackValue" Height="300" Width="300">
<Window.Resources>
<t:TestCollection x:Key="test"/>
<CompositeCollection x:Key="MyItemsSource">
<x:Static Member="t:TestCollection.NullItem"/>
<CollectionContainer Collection="{Binding Source={StaticResource test}}"/>
</CompositeCollection>
<t:TestModel x:Key="model"/>
<t:NullItemConverter x:Key="nullItemConverter"/>
</Window.Resources>
<StackPanel>
<ComboBox x:Name="cbox" ItemsSource="{Binding Source={StaticResource MyItemsSource}}" IsEditable="true" IsReadOnly="True" Text="Select an Option" SelectedItem="{Binding Source={StaticResource model}, Path=TestItem, Converter={StaticResource nullItemConverter}, ConverterParameter={x:Static t:TestCollection.NullItem}}"/>
<TextBlock Text="{Binding Source={StaticResource model}, Path=TestItem, TargetNullValue='Testitem is null'}"/>
</StackPanel>
Basically, the pattern is that you declare a singleton NullInstance of the class you use as items, and use a Converter which converts this instance to null when setting the VM property. The converter can be written universally, like this (it's VB, I hope you don't mind):
Public Class NullItemConverter
Implements IValueConverter
Public Function Convert(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert
If value Is Nothing Then
Return parameter
Else
Return value
End If
End Function
Public Function ConvertBack(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
If value Is parameter Then
Return Nothing
Else
Return value
End If
End Function
End Class
Since you can reuse the converter, you can set this all up in XAML; the only thing that remains to be done in code is to provide the singleton NullItem.

Personally, I tend to add an "empty" version of whatever object is in my collection I'm binding to. So, for example, if you're binding to a list of strings, then in your viewmodel, insert an empty string at the beginning of the collection. If your Model has the data collection, then wrap it with another collection in your viewmodel.
MODEL:
public class Foo
{
public List<string> MyList { get; set;}
}
VIEW MODEL:
public class FooVM
{
private readonly Foo _fooModel ;
private readonly ObservableCollection<string> _col;
public ObservableCollection<string> Col // Binds to the combobox as ItemsSource
{
get { return _col; }
}
public string SelectedString { get; set; } // Binds to the view
public FooVM(Foo model)
{
_fooModel = model;
_col= new ObservableCollection<string>(_fooModel.MyList);
_col.Insert(0, string.Empty);
}
}

You could also extend the ComboBox to enable de-selecting. Add one or more hooks (eg, pressing the escape key) that allow the user to set the SelectedItem to null.
using System.Windows.Input;
public class NullableComboBox : ComboBox
{
public NullableComboBox()
: base()
{
this.KeyUp += new KeyEventHandler(NullableComboBox_KeyUp);
var menuItem = new MenuItem();
menuItem.Header = "Remove selection";
menuItem.Command = new DelegateCommand(() => { this.SelectedItem = null; });
this.ContextMenu = new ContextMenu();
this.ContextMenu.Items.Add(menuItem);
}
void NullableComboBox_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Escape || e.Key == Key.Delete)
{
this.SelectedItem = null;
}
}
}
Edit Just noticed Florian GI's comment, the Context Menu might be another good deselect hook to add.

One option would be to create an adapter collection that you expose specifically for consumers that want an initial 'empty' element. You would need to create a wrapper class that implements IList (if you want same performance as with ObservableCollection) and INotifyCollectionChanged. You would need to listen to INotifyCollectionChanged on the wrapped collection, then rebroadcast the events with indices shifted up by one. All of the relevant list methods would also need to shift indices by one.
public sealed class FirstEmptyAdapter<T> : IList<T>, IList, INotifyCollectionChanged
{
public FirstEmptyCollection(ObservableCollection<T> wrapped)
{
}
//Lots of adapter code goes here...
}
Bare minimum if you want to avoid the IList methods is to implement INotifyCollectionChanged and IEnumerable<T>.

One simple approach is to re-template the ComboBox so that when there is an item select a small X appears on the right side of the box. Clicking that clears out the selected item.
This has the advantage of not making your ViewModels any more complicated

Related

How to filter combobox items based on the input text?

I would like to display a combo box whose items are supplied by the view model.
The combo box should be editable.
Based on the text currently input by the user, the combo box items should be filtered.
I am trying to apply the following solution pointed out in various resources on the topic (such as that question, that question, that article, that question, that blogpost, that tutorial, etc.):
My view model provides a collection view around the items.
I have two-way bound the Text property of the combo box to a CustomText property in my view model.
The Filter predicate on the collection view is set to check items based on whether their display name contains the CustomText.
When CustomText is changed, the Refresh method on the items collection view is invoked.
I'd expect this to update the list of items in the combo box dropdown list whenever I modify the text. Unfortunately, the list remains the same.
If I place a breakpoint in my Filter predicate, it gets hit, but somehow, not always for each item.
Here is a minimal example:
Xaml for the window:
<Window x:Class="ComboBoxFilterTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ComboBoxFilterTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ComboBox
VerticalAlignment="Center"
ItemsSource="{Binding Items}"
DisplayMemberPath="Name"
IsEditable="True"
Text="{Binding CustomText}"
IsTextSearchEnabled="False"/>
</Grid>
</Window>
The code-behind for the window:
using System.Windows;
namespace ComboBoxFilterTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
}
And the view model (here with the Item data class, which would normally reside elsewhere):
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
namespace ComboBoxFilterTest
{
public class MainViewModel : INotifyPropertyChanged
{
private sealed class Item
{
public int Id { get; set; }
public string Name { get; set; }
}
public MainViewModel()
{
Items = new CollectionView(items)
{
Filter = item =>
{
if (string.IsNullOrEmpty(customText))
{
return true;
}
if (item is Item typedItem)
{
return typedItem.Name.ToLowerInvariant().Contains(customText.ToLowerInvariant());
}
return false;
}
};
}
private readonly ObservableCollection<Item> items = new ObservableCollection<Item>
{
new Item{ Id = 1, Name = "ABC" },
new Item{ Id = 2, Name = "ABCD" },
new Item{ Id = 3, Name = "XYZ" }
};
public ICollectionView Items { get; }
private string customText = "";
public event PropertyChangedEventHandler PropertyChanged;
public string CustomText
{
get => customText;
set
{
if (customText != value)
{
customText = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomText)));
Items.Refresh();
}
}
}
}
}
Basically, I think I'm doing the same as what is described in another question, but apparently, something is still different as it doesn't work in my case.
Note that one slight difference is that I am not using CollectionViewSource.GetDefaultView, as I want to have several differently filtered views on the same collection rather than obtaining the default view.
Note that as a workaround, I could of course just return the appropriately filtered enumerable of items myself and fire a property changed event for such an enumerable property each time the filter changes. However, I understand relying on collection views is the proper WPF way, so I would prefer to do it "correctly".
I think I found a solution: As was suggested in an answer on a related topic, I used ListCollectionView instead of CollectionView.
For some reason, it works with ListCollectionView while it doesn't with CollectionView, even though the latter does not give any indication that it shouldn't (e.g. CollectionView.CanFilter returns true).
I am going to accept this answer of my own for now, though if someone can provide an answer that actually provides an explanation for this behaviour, I'll be glad to accept such an answer instead.
The recommended pattern to avoid any problems like the one you are experiencing is to use CollectionViewSource as binding source.
As also mentioned in the docs, you should never create instances of CollectionView manually. You have to use a specialized sub-type according to the actual type of the source collection:
"You should not create objects of this class [CollectionView] in your code. To create a collection view for a collection that only implements IEnumerable, create a CollectionViewSource object, add your collection to the Source property, and get the collection view from the View property." Microsoft Docs: CollectionView
CollectionViewSource internally does the type checking for you and creates a properly initialized ICollectionView implementation, that is appropriate for the actual source collection. CollectionViewSource, whether created in XAML or C#, is the recommended way to obtain an instance of ICollectionView, if the default view is not sufficient:
public ICollectionView Items { get; }
public CollectionViewSource ItemsViewSource { get; }
public ctor()
{
ObservableCollection<object> items = CreateObservableItems();
this.ItemsViewSource = new CollectionViewSource() {Source = items};
this.Items = this.ItemsViewSource.View;
}

How to pass parameter to ComboBox ItemsSource binding?

I am working with WPF for the first time, so please bear with me.
I have a combobox, which is meant to generically display some lookup data. The models for the different types of lookups are exactly the same, just different data sources which are retrieved via a single method call passing different enumerations to control the returned data set. Fairly simple stuff.
public sealed class MyDataProvider
{
public enum Types
{
Green,
Blue,
Orange
}
private readonly ConcurrentDictionary<string, ObservableCollection<LookUpVm>> _lookupData =
new ConcurrentDictionary<string, ObservableCollection<LookUpVm>>();
private static readonly Lazy<MyDataProvider> lazy =
new Lazy<MyDataProvider>(() => new MyDataProvider());
public static MyDataProvider Instance => lazy.Value;
private MyDataProvider()
{
}
public ObservableCollection<LookUpVm> GreenLookupDataSource => GetLookupDataSource(Types.Green);
public ObservableCollection<LookUpVm> GetLookupDataSource(Types lookupEnum)
{
ObservableCollection<LookUpVm> lookupDataSource;
if (_lookupData.TryGetValue(lookupEnum, out lookupDataSource))
return lookupDataSource;
lookupDataSource = new ObservableCollection<LookUpVm>();
var returnedlookupDataSource =
SomeMasterSource.GetlookupDataSourceBylookupEnum(lookupEnum).OrderBy(ia => ia.Name);
foreach (var returnedLookupData in returnedlookupDataSource)
{
lookupDataSource.Add(returnedLookupData);
}
_lookupData.TryAdd(lookupEnum, lookupDataSource);
return lookupDataSource;
}
}
This works great for the 0th iteration, where I create a GreenLookupComboBox.
<ComboBox ItemsSource="{Binding Source={x:Static objectDataProviders:MyDataProvider.Instance},
Path=GreenLookupDataSource}" />
However, what I really need to be able to do is to set up a combobox which can have its Types enum value set on the parent View, which would then call directly to the GetLookupDataSource and pass the enum. We have several dozen lookup types, and defining a new property for each feels less than ideal. Something like the below for the control view...
<ComboBox ItemsSource="{Binding Source={x:Static objectDataProviders:MyDataProvider.Instance},
Path=GetLookupDataSource}" />
And something like the below for where I use the lookup control.
<local:MyLookupControl Type=Types.Green />
Is this even possible?
EDIT:
Here's an example of what I'm trying to accomplish.
I have two key-value pairs of lists.
ListOne
1 - A
2 - B
3 - C
and
ListTwo
1 - X
2 - Y
3 - Z
They are accessible by calling the method GetList(Enum.LookupType). They share the same ViewModel and View. However, I need to place both of them on a form for my users to select from.
I'm looking for some way to use XAML like the following on the View they appear on.
<local:MyLookupControl Method=GetList Parameter=Enum.ListOne/>
<local:MyLookupControl Method=GetList Parameter=Enum.ListTwo />
This should display a pair of comboboxes, one bound to ListOne and one bound to ListTwo.
You're essentially just trying to set up databinding on a couple controls. This is simple then so long as you have the correct datacontext for the view.
Controls can be bound to properties (which is exactly what you are looking for).
Using your edited example here is how you would do that:
private ObservableCollection<string> _listOne;
private ObservableCollection<string> _listTwo;
private string _selectedListOneItem;
private string _selectedListTwoItem;
public ObservableCollection<string> ListOne
{
get { return _listOne; }
set { _listOne = value; }
}
public ObservableCollection<string> ListTwo
{
get { return _listTwo; }
set { _listTwo = value; }
}
public string SelectedListOneItem
{
get { return _selectedListOneItem; }
set { _selectedListOneItem = value; }
}
public string SelectedListTwoItem
{
get { return _selectedListTwoItem; }
set { _selectedListTwoItem = value; }
}
XAML:
<ComboBox ItemsSource="{Binding ListOne}" SelectedItem="{Binding SelectedListOneItem}"/>
<ComboBox ItemsSource="{Binding ListTwo}" SelectedItem="{Binding SelectedListTwoItem}"/>
You have several options in how you want to load or get your lists. You can either load them in the constructor or for something a bit heavier is load them every time you "get" in the property. I would recommend loading those in the constructor.
What I provided is basically autoprops and can even be further simplified but I wanted to show you that you can also write code in the getter and setter of those properties to further expand on the items. For instance, you may want something on the background to fire off when SelectedListOneItem changes. In this case on the SET of SelectedListOneItem you can set the value but then also run a method/function which may update other properties.
WPF is very dependent on properties to bind between ViewModels and Views. In your response before the edit you are using fields which can't be bound to controls in a view.
EDIT:
If you do plan on updating properties in the ViewModel that would change things on the view you are also going to want to look into INotifyPropertyChanged. By implementing INotifyPropertyChanged the view will be updated/notified when properties are changing. INotifyPropertyChanged comes with it's own event that you must invoke in the setting of a property. Here is also a very helpful method you can call that will fire this event for you much easier.
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
You call this like so:
public string SelectedListOneItem
{
get { return _selectedListOneItem; }
set
{
_selectedListOneItem = value;
OnPropertyChanged();
}
}
That way if anything else in the ViewModel updates SelectedListOneItem that your view will make the appropriate change. In this case it would make the combobox select the new value you set in SelectedListOneItem.

equivalent code in wpf

In Winforms I used the below code to select the specific item in DataGridView.
If DGView.Rows(row).Cells(0).Value.StartsWith(txtBoxInDGView.Text, StringComparison.InvariantCultureIgnoreCase) Then
DGView.Rows(row).Selected = True
DGView.CurrentCell = DGView.SelectedCells(0)
End If
Can anyone give the equivalent code for WPF DataGrid?
WPF is more data-driven than WinForms. It means it's better to work with objects (that represent your data) than to deal with UI elements.
You should have a collection that is the items source of the data grid. In the same data context, you should have a property that will hold the selected item (same type as the items in the collection). All properties should notify change.
Considering you have MyItem class for each row in data grid, the code would be something like this:
In the class that is the data context of your data grid:
public ObservableCollection<MyItem> MyCollection {get; set;}
public MyItem MySelectedItem {get; set;} //Add change notification
private string _myComparisonString;
public string MyComparisonString
{
get{return _myComparisonString;}
set
{
if _myComparisonString.Equals(value) return;
//Do change notification here
UpdateSelection();
}
}
.......
private void UpdateSelection()
{
MyItem theSelectedOne = null;
//Do some logic to find the item that you need to select
//this foreach is not efficient, just for demonstration
foreach (item in MyCollection)
{
if (theSelectedOne == null && item.SomeStringProperty.StartsWith(MyComparisonString))
{
theSelectedOne = item;
}
}
MySelectedItem = theSelectedOne;
}
In your XAML, you'd have a TextBox and a DataGrid, similar to this:
<TextBox Text="{Binding MyComparisonString, UpdateSourceTrigger=PropertyChanged}"/>
....
<DataGrid ItemsSource="{Binding MyCollection}"
SelectedItem="{Binding MySelectedItem}"/>
This way, your logic is independent from your UI. As long as you have change notification - the UI will update the properties and the properties will affect the UI.
[Treat code above as a pseudo-code, I'm not on my dev machine currently]

Binding to an initially NULL property in the ViewModel doesn't rebind

I have a UserControl that contains a Telerik RadDataForm. The form's ItemsSource is bound to a property on the UserControl's ViewModel:
<telerik:RadDataForm
ItemsSource="{Binding Path=viewModel.items, RelativeSource={RelativeSource AncesterType=local:MyUserControl}}"
/>
Where viewModel is:
public partial class MyUserControl: UserControl
{
public MyUserControlVM viewModel
{ get { return this.DataContext as MyUserControlVM; } }
}
Within the viewmodel, items is a fairly ordinary collection:
public class MyUserControlVM : MyViewModelBase
{
private ObservableCollection<AnItem> items_;
public ObservableCollection<AnItem> items
{
get { return this.items_; }
set
{
this.items_ = value;
notifyPropertyChanged("items");
}
}
...
}
And where, of course, MyViewModelBase implements INotifyPropertyChanged.
The user control has an items dependency property, and when it is set, it sets the matching property on the view model:
public partial class MyUserControl : UserControl
{
public ObservableCollection<AnItem> items
{
get { return GetValue itemsProperty as ObservableCollection<AnItem>; }
set { SetValue(itemsProperty, value); }
}
public static readonly DependencyProperty itemsProperty =
DependencyProperty.Register("items",
typeof(ObservableCollection<AnItem>),
typeof(MyUserControl), new PropertyMetadata(
new PropertyChangedCallback(itemsPropertyChanged)));
private static void itemsPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
MyUserControl myUserControl = d as MyUserControl;
ObservableCollection<AnItem> items =
e.NewValue as ObservableCollection<AnItem>;
if (myUserControl != null && myUserControl.viewModel != null)
myUserControl.viewModel.items = items;
}
}
All of which seems pretty straightforward, if a bit tedious.
The problem is that the items dependency property on MyUserControl is bound to a property of the current item of another collection, and that the current item is initially null, and so when MyUserControl is initially loaded, its items property is null. And hence, so is the items property on MyUserControlVM that the RadDataForm is binding to.
Later, when an item in that outer collection is made current, the items dependency property on MyUserControl is set, and that sets the items property on MyUserControlVM. And MyUserControlVM calls notifyPropertyChanged so that listeners will be informed of the change. But this last is not working.
Afterwards, if I examine RadDataForm, its ItemsSource property is still null.
It's like the RadDataForm isn't listening for the propertychanged event, because what it was bound to was initially null. In similar circumstances where the bound property is not null at the start, this pattern works fine as the current item changes from one item to another, but it doesn't seem to work from having no current item to having one.
So, any ideas as to how to make this work? I can't, given the circumstances, make it so that items always has a value when the form loads - it is always going to be null, at the start. How do I get the RadDataForm to notice when the property becomes non-null?
When I want to reference something at the root of my UserControl (a custom property, for instance, or as in your case, the DataContext), I usually give my UserControl a Name. Then I use this name together with the ElementName property on the Binding to set it up.
<UserControl
...
Name="TheControl">
<Grid>
<TextBlock Text={Binding Path=DataContext.items, ElementName=TheControl}" />
</Grid>
</UserControl>
Due to the viewModel property, you can use that and DataContext interchangeably.
However, in your case it might actually be simpler. First, there's a typo in your code. It should be AncestorType (with an 'o'). Second, you might want to try setting up the binding using only {Binding Path=items} since I believe that your control already inherits the correct DataContext. (Not sure about that last one, though.)
If the problem persists, and you suspect that it does in fact has something to do with the items property returning null initially, you could always initialize the items_ with an empty collection to avoid the null.
private ObservableCollection<AnItem> items_ = new ObservableCollection<AnItem>();

WPF Binding to a Combo using only a subset of a Collection's items

I'm trying to set a TwoWay binding to a combo box using only a selection of a collection's objects. Currently, everything works out ok if I just want to bind everything in the colelction, but in the example class below, what if I only want to show items where Active=True? I can filter the items using LINQ like ItemsSource = FROM x IN Coll WHERE x.Active=True but then I lose the TwoWay binding. Ie, if the name or the active state in the source is updated from elsewhere, the combo box does not automatically update.
Is the possible to do? If not, does anyone who has had to deal with this have some suggestions?
'The Class
Public Class Test
Implements ComponentModel.INotifyPropertyChanged
Private _Name As String
Private _Active As Boolean
Public Sub New(Name As String, Active As Boolean)
_Name=Name
_Active=Active
End Sub
Public Property Name() As String
End Class
'Declare a Collection and add some Tests, then bind to Cbo in Page Load
Dim Coll As New ObservableCollection
Coll.Add(New Test("Test1", True))
Coll.Add(New Test("Test2", False))
Coll.Add(New Test("Test3", True))
TheComboBox.ItemsSource=Coll
Two options:
You can use a framework like Bindable LINQ that makes your LINQ queries return observable collections (thus the binding stays as two-way).
Or you could bind your ComboBox's items to a CollectionViewSource and run each item through a Filter event handler:
<CollectionViewSource
Source="{Binding MyItems}"
Filter="OnlyActiveItems"
x:Key="ItemsView"/>
<ComboBox ItemsSource="{Binding Source={StaticResource ItemsView}}" />
with code-behind:
private void OnlyActiveItems(object sender, FilterEventArgs e)
{
e.Accepted = false;
var item = e.Item as Text;
if (item == null) return;
e.Accepted = item.Active;
}
Note that I'm not entirely sure that a CollectionViewSource will recognise the INotifyPropertyChanged interface and re-query the list when one element changes. I'd really suggest Bindable LINQ if the filter approach doesn't work.
The CollectionViewSource class can add sorting and filtering to any WPF items control

Resources