I have searched Google for a simple solution to this but no luck. I have a standard WPF combo box which I would simply like to be able to filter the list displayed according to the first 2 or 3 letters a users types when the combo box has focus. I tried some coding including some lamba expressions but the error "System.NotSupportedException" keeps getting thrown on the line where "combobox.Items.Filter" is specified. I'm not using MVVM and would just like this simple functionality available for the user. Please help! P.S. IsEditable, IsTextSearchEnabled and StaysOpenOnEdit properties are set to true but the desired functionality is not yet achieved.
I have developed a sample application. I have used string as record item, you can do it using your own entity. Backspace also works properly.
public class FilterViewModel
{
public IEnumerable<string> DataSource { get; set; }
public FilterViewModel()
{
DataSource = new[] { "india", "usa", "uk", "indonesia" };
}
}
public partial class WinFilter : Window
{
public WinFilter()
{
InitializeComponent();
FilterViewModel vm = new FilterViewModel();
this.DataContext = vm;
}
private void Cmb_KeyUp(object sender, KeyEventArgs e)
{
CollectionView itemsViewOriginal = (CollectionView)CollectionViewSource.GetDefaultView(Cmb.ItemsSource);
itemsViewOriginal.Filter = ((o) =>
{
if (String.IsNullOrEmpty(Cmb.Text)) return true;
else
{
if (((string)o).Contains(Cmb.Text)) return true;
else return false;
}
});
itemsViewOriginal.Refresh();
// if datasource is a DataView, then apply RowFilter as below and replace above logic with below one
/*
DataView view = (DataView) Cmb.ItemsSource;
view.RowFilter = ("Name like '*" + Cmb.Text + "*'");
*/
}
}
XAML
<ComboBox x:Name="Cmb"
IsTextSearchEnabled="False"
IsEditable="True"
ItemsSource="{Binding DataSource}"
Width="120"
IsDropDownOpen="True"
StaysOpenOnEdit="True"
KeyUp="Cmb_KeyUp" />
I think the CollectionView is what you are looking for.
public ObservableCollection<NdfClassViewModel> Classes
{
get { return _classes; }
}
public ICollectionView ClassesCollectionView
{
get
{
if (_classesCollectionView == null)
{
BuildClassesCollectionView();
}
return _classesCollectionView;
}
}
private void BuildClassesCollectionView()
{
_classesCollectionView = CollectionViewSource.GetDefaultView(Classes);
_classesCollectionView.Filter = FilterClasses;
OnPropertyChanged(() => ClassesCollectionView);
}
public bool FilterClasses(object o)
{
var clas = o as NdfClassViewModel;
// return true if object should be in list with applied filter, return flase if not
}
You wanna use the "ClassesCollectionView" as your ItemsSource for your Combobox
Related
I have a winform that includes a ListBox and a Combobox. In this ListBox appears a clients list on first run.
I want to filter the "Clients" in the ListBox with a Combobox.
To fill the ListBox using the string selected in the Combobox i'm using :
private void FillListBox()
{
this.lstClient.Items.Clear();
foreach (Client c in this.client)
{
if (this.cBox.Text == "All")
this.lstClient.Items.Add(c.ToString());
else
if (this.cBox.Text == "Retail" && c.GetType() == typeof(RetailClient))
this.lstClient.Items.Add(c.ToString());
}
this.lstClient.Sorted = true;
}
After that i call this method from the event of the ComboBox :
private void cBox_TextChanged(object sender, EventArgs e)
{
this.FillListBox();
}
It works "great" but my code is not really dynamic and too long (lots of differents clients) that's why i would like to use LINQ.
I read on microsoft's documentation but i'm pretty confused on how using it.
Does anyone have some times to show me the way ?
Adding infos :
My form :
I select the type i want in the ComboBox :
The result :
Thanks
Ok let's give it a try. If you want to implement filtering you should think about a proper structure how to represent your filter criterion. In this case you have a label in your combobox which is bound to a unique filter criterion. This could be represented by a custom class:
public class SortingRepresentation
{
public string DisplayLabel { get; set; }
public Type ClientType { get; set; }
}
Now you can create a List of those criteria and shove it into the combobox:
List<SortingRepresentation> sortingFields = new List<SortingRepresentation>();
public Form1()
{
sortingFields.Add(new SortingRepresentation{ DisplayLabel = "All", TypeCriterion = typeof(Client) });
sortingFields.Add(new SortingRepresentation{ DisplayLabel = "Only Retail", TypeCriterion = typeof(Client_A) });
sortingFields.Add(new SortingRepresentation{ DisplayLabel = "Only Wholesale", TypeCriterion = typeof(Client_B) });
sortingFields.Add(new SortingRepresentation{ DisplayLabel = "Only Human Wholesale", TypeCriterion = typeof(Client_C) });
cBox.DisplayMember = "DisplayLabel";
cBox.DataSource = sortingFields;
}
When the selection changes in the combobox you can catch now the selected item (which will be of type SortingRepresentation and pass it as a filter to FillListBox:
private void cBox_SelectedIndexChanged(object sender, EventArgs e)
{
FillListBox((SortingRepresentation)cBox.SelectedItem);
}
Now you can use the Type TypeCriterion inside this object to filter your list:
private void FillListBox(SortingRepresentation sortcriterion)
{
this.lstClient.DataSource = null;
this.lstClient.DataSource = client
.Where(x => x.GetType() == sortcriterion.TypeCriterion || // either you are of this type
x.GetType().BaseType == sortcriterion.TypeCriterion // or your parent is of this type
).ToList();
}
Since you are using a listbox, you can bind the sorted list directly to the DataSource and be done with it. For the proper display, you need to override the ToString method in your Client class and the ListBox will take care of the display accordingly. But as I see you've done it already
I am working with a xamarin Forms.
I am using Picker for DropDownList.
How can I set selectedItem to Picker?
My code
<Picker x:Name="VendorName" Title="Select" ItemDisplayBinding="{Binding VendorName}" SelectedItem="{Binding VendorName}" Style="{StaticResource PickerStyle}"></Picker>
and server side code is
Device.BeginInvokeOnMainThread(() =>
{
VendorName.ItemsSource = VendorList;
});
var currentVendor = new List<Vendor>();
currentVendor.Add(new Vendor { VendorID = "111", VendorName = "aaaa" });
VendorName.SelectedItem = currentVendor;
This may not be the most efficient but you could loop to find the index and set that way.
for (int x = 0; x < VendorList.Count; x++)
{
if (VendorList[x].VendorName == currentVendor .VendorName )
{
VendorName.SelectedIndex = x;
}
}
After adding all values as list in Picker
just treat with it as an array
so if you want to set selected item just set selected item index
currentVendor.SelectedIndex = 0;
zero means you make selected item is the first one you added to Picker
If you are using MVVM, and want to set SelectedItem from the view model, things get tricky. There seems to be a bug in Xamarin that prevents us from using SelectedItem with a two way binding. More info: Xamarin Forms ListView SelectedItem Binding Issue and https://xamarin.github.io/bugzilla-archives/58/58451/bug.html.
Luckily, we can easily write our own Picker.
public class TwoWayPicker : Picker
{
public TwoWayPicker()
{
SelectedIndexChanged += (sender, e) => SelectedItem = ItemsSource[SelectedIndex];
}
public static new readonly BindableProperty SelectedItemProperty = BindableProperty.Create(
nameof(SelectedItem), typeof(object), typeof(TwoWayPicker), null, BindingMode.TwoWay, propertyChanged: OnSelectedItemChanged);
public new object SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}
private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (TwoWayPicker)bindable;
control.SetNewValue(newValue);
}
private void SetNewValue(object newValue)
{
if (newValue == null)
{
return;
}
for(int i = 0; i < ItemsSource.Count; i++)
{
if (ItemsSource[i].Equals(newValue))
{
SelectedIndex = i;
return;
}
}
}
}
Because is uses the same SelectedItem property, it is a drop-in replacement for Picker.
Note that if you want value equality rather than reference equality for the item class, you'll also need to override Equals like this:
public override bool Equals(object obj)
{
var other = obj as YourClass;
if (other == null)
{
return false;
}
else
{
return other.SomeValue == SomeValue; // implement your own
}
}
If you define the item class as a record instead of a class then it can select the item programmatically using the SelectedItem property.
In your case change
public class Vendor { // your class properties }
to
public record Vendor { // your class properties }
This will now work
VendorName.SelectedItem = currentVendor;
i just started learning WPF as i am moving on from WinForm. At the moment i am having difficulties displaying bind data from class to tree view.
My tree view works perfectly if i use .Items.Add() method but when it comes to binding class data to TreeView this is what i see:
Here is the c# code:
public MainWindow()
{
InitializeComponent();
Search sc = new Search();
sc.query(null, "");
this.DataContext = sc;
}
Here is the xaml
<TreeView Width="400" Height="500" Name="TreeViewB" ItemsSource="{Binding getTreeResults}" Style="{StaticResource myTreeView}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Network}">
<TextBlock Text="{Binding getNetwork}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
Edited - 2 class added
Here is my class A
class Social_Searcher
{
List<Social_Network> networks = new List<Social_Network>();
public List<Social_Network> getTreeResults { get { return networks; } }
}
Here is my class B
class Social_Network
{
private string network_name;
private List<Keypair> data;
public Social_Network()
{
data = new List<Keypair>();
}
public struct Keypair
{
public void add(string _name, string _value)
{
name = _name;
value = _value;
}
public string name, value;
}
public string Network
{
get { return network_name; }
set { network_name = value; }
}
public void add(string name, string value)
{
if (name == "network")
{
network_name = value;
}
Keypair kp = new Keypair();
kp.add(name, value);
data.Add(kp);
}
public string getNetwork()
{
return network_name;
}
public List<Keypair> getData()
{
return data;
}
public string findKey_value(string key)
{
foreach (Keypair kp in data)
{
if (kp.name == key) return kp.value.ToString();
}
return "null";
}
}
You don't give much code, but getTreeResults and getNetwork look like methods, and your TextBlock will not know how to present them (normally, it would use the results of ToString(), but I don't know if that will work with a method.
If you want those methods, you can try it this way:
public string TreeResults { get { return sc.getTreeResuls(); }}
and then
<TreeView ... ItemsSource={Binding TreeResults} ... > ...
The same goes for getNetwork. I.e., you wrap each method in a public property.
If you don't want to do that, or can't, you can use an IValueConverter
There is clearly something going on in your UI, but it's hard to tell what exactly.
You will likely find a debugging tool such as Snoop useful, as it will allow you to click on items in your UI and see how they exist in the logical tree. You can modify their properties while the program is running to experiment and learn what you need to change in your source code.
I ran into this issue when converting a Windows Forms application to WPF. I know it sounds ridiculous, but make sure that your value is stored in the TreeViewItem's "Header" property, NOT the "Name" property. Once I did this, my list populated as expected.
I was following this link here: http://jacobmsaylor.com/?p=1270
but i'm having problems with it, trying to make tweaks to it
<ListBox Name="PageList_ListBox" MouseDoubleClick="PageList_ListBox_OnMouseDoubleClick"
Background="#FFC9C9C9" Margin="0,5,0,0" ItemsSource="{Binding PageCollection, ElementName=This}">
.
public static ObservableCollection<MLBPage> _PageCollection = new ObservableCollection<MLBPage>();
public static ObservableCollection<MLBPage> PageCollection
{
get { return _PageCollection; }
}
public ICollectionView _PageCollectionView { get; set; }
_PageCollectionView = CollectionViewSource.GetDefaultView(_PageCollection);
private bool FilterLeadersList(object item)
{
MLBPage page = item as MLBPage;
if (page.templateName.Contains("Leaders List"))
{
return true;
}
else
{
return false;
}
}
My MLBPage object has 2 types... where the "templateName" can be either "Leaders List" or "Leader Headshots".. now when I filter the collection by adding to a button:
_PageCollectionView.Filter = FilterLeadersList;
the whole collection just filters (the _PageCollection binded to a listbox turns blank) instead of only the items that contain "Leaders List" within the name....
any help on how i can modify this to work?
change your code into:
private ObservableCollection<MLBPage> _PageCollection = new ObservableCollection<MLBPage>();
public ICollectionView _PageCollectionView { get; set; }
just do this once (eg. within ctor)
//ctor
_PageCollectionView = CollectionViewSource.GetDefaultView(_PageCollection);
_PageCollectionView.Filter = FilterLeadersList,
use clear, add , remove to alter your _PageCollection.
bind your listbox to your view
<ListBox ItemsSource="{Binding _PageCollectionView}"/>
use Refresh to refresh your filter
_PageCollectionView.Refresh();
I recently added a Disabled property to one of my domain entities. I have a List<T> of those entities in a view model. I just changed the getter to filter the list based on a UI setting to selectively show items that are disabled.
public ObservableCollection<CustomVariableGroup> CustomVariableGroups
{
get
{
if (this.ShowDisabled) { return this._customVariableGroups; }
return new ObservableCollection<CustomVariableGroup>(this._customVariableGroups.Where(x => x.Disabled == false));
}
}
The problem is that when I now try to do this (in the same view model), the item doesn't get added to the collection.
this.CustomVariableGroups.Add(newGroup);
And I think I know why: The newGroup is being added to the new ObservableCollection<T>, not the backing field of _customVariableGroups.
If the LINQ Where() extension method was able to return ObservableCollection<T> (the type of the private backing field itself) instead of IEnumerable, I don't think I'd have this issue. As it stands now, since Where() returns IEnumerable<T>, I need to wrap that inside a new ObservableCollection<T>.
What is the right/best way for me to add a new item within the same view model? Do I need to add it directly to the private backing field? I don't want to do that because I don't want to have to remember not to use the property itself.
instead of binding to your collection directly, you could bind to a ICollectionView and set a Filter in your Disabled property. no need to alter the source collection.
EDIT:
viewmodel:
//this collection just init once - eg. in ctor, use add, remove, clear to alter
public ObservableCollection<CustomVariableGroup> CustomVariableGroups {get; private set; }
//create just once in ctor
public ICollectionView MyView {get; private set;}
//ctor
this.CustomVariableGroups = new ObservableCollection<CustomVariableGroup>();
this.MyView = CollectionViewSource.GetDefaultView(this.CustomVariableGroups);
//in your disabled property set the filter
public bool ShowDisabled
{
get { return _showDisabled; }
set {
_showDisabled = value;
if (_showDisabled)
//show just disabled
this.MyView.Filter = (item) =>
{
var myitem = (CustomVariableGroup) item;
return myitem.Disabled;
};
else
{
//show all
this.MyView.Filter = (item) => { return true; };
}
this.NotifyPropertyChanged("ShowDisabled"); }
}
xaml
<CheckBox Content="ShowDisabled" IsChecked="{Binding ShowDisabled}"/>
<ListBox ItemsSource="{Binding MyView}" Grid.Row="1">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
this works in my testproject
As mentioned in the comments, the right way to solve this is in the view, not the view model. This is a view concern only. I thought I'd post how I handled it, just in case this helps someone else.
First, the getter (view model) should just return the full list every time:
public ObservableCollection<CustomVariableGroup> CustomVariableGroups
{ get { return this._customVariableGroups; } }
Then, in the view, I added the filter event to my CollectionViewSource:
<CollectionViewSource x:Key="SortedGroups" Source="{Binding CustomVariableGroups}" Filter="CollectionViewSource_Filter">
And to get the list to refresh every time I clicked the Show Disabled checkbox:
Command="{Binding RefreshVariableGroupCommand}"
Then, in the code behind, I implemented the filter:
private void CollectionViewSource_Filter(object sender, FilterEventArgs e)
{
CustomVariableGroup customVariableGroup = e.Item as CustomVariableGroup;
if (customVariableGroup == null) { return; }
if ((bool)chkShowDisabled.IsChecked)
{
// Show everything
e.Accepted = true;
return;
}
// We are not showing disabled items, so set disabled items e.Accepted to false.
if (customVariableGroup.Disabled == true)
{
e.Accepted = false;
return;
}
e.Accepted = true;
}
The only part about this, that I don't like, is that I'm using MVVM and trying to avoid code-behind classes. But, it's better to have this than hack the view model.
I would maintain your backing List and then raise the property changed for your ObservableCollection.
public ObservableCollection<CustomVariableGroup> CustomVariableGroups
{
get
{
if (this.ShowDisabled) { return this._customVariableGroups; }
return new ObservableCollection<CustomVariableGroup> (this._customVariableGroups.Where(x => x.Disabled == false));
}
}
// adds to the backing list but raises on property to rebuild and
// return the ObservableCollection
public void AddCustomVariableGroup(CustomVariableGroup newGroup)
{
this._customVariableGroups.Add(newGroup);
OnPropertyChanged("CustomVariableGroups");
}
//Example invocation
AddCustomVariableGroup(newGroup);
I had a similar problem with a WinPhone app when showing filtered result. My solution was to bind to the "ShowDisabled" property change and on this event simply Clear() and readd stuff into the main Observable: CustomVariableGroups in this case. Something like this:
public bool ShowDisabled
{
get
{
return showDisabled;
}
set
{
if (showDisabled!= value)
{
showDisabled = value;
NotifyPropertyChanged("ShowDisabled");
}
}
}
In the constructor add this: this.PropertyChanged += new PropertyChangedEventHandler(MyViewModel_PropertyChanged);
And in the Event Handler:
void MyViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "ShowDisabled")
{
CustomVariableGroups.Clear();
foreach (var cvg in AllVariableGroups.Where(x => !x.Disabled))
{
CustomVariableGroups.Add(cvg);
}
}
}
The component bound to the CustomVariableGroups should update accordingly.
Disclaimer: of course there is a performance impact in this and you need to decide if it is negligible or not.
I would do something like #Tallmaris, but instead of this in the MyViewModel_PropertyChanged:
CustomVariableGroups.Clear();
foreach (var cvg in CustomVariableGroups.Where(x => !x.Disabled))
{
CustomVariableGroups.Add(cvg);
}
Do this:
foreach(var cvg in CustomVariableGroups.Where( x => x.Disabled))
{
CustomVariableGroups.Remove(cvg);
}
And if that doesn't work (depends if Enumerable.Where uses iterator blocks, I think), you can do this:
foreach(var cvg in Custom VariableGroups.Where( x=> x.Disabled).ToList())
{
CustomVariableGroups.Remove(cvg);
}