I am creating a WPF app containing a ComboBox which shows some data. I want to use the combobox-integrated text seach. But the problem is, if the user searchs for "llo", the list should show all items, containing this text snippet, like "Hallo", "Hello", "Rollo" ... But the search returns no result because the property name of no item starts with "llo". Has somebody an idea how to achieve this?
I am using the MVVM-pattern. The view is binded to a collection of DTOs (property of the viewmodel), in the DTO there are two properties which are relevant for the search.
<ComboBox
ItemsSource="{Binding Path=Agencies}"
SelectedItem="{Binding Path=SelectedAgency}"
IsTextSearchEnabled="True"
DisplayMemberPath="ComboText"
IsEnabled="{Binding IsReady}"
IsEditable="True"
Grid.Column="0"
Grid.Row="0"
IsTextSearchCaseSensitive="False"
HorizontalAlignment="Stretch">
</ComboBox>
public class Agency
{
public int AgencyNumber { get; set; }
public string Title { get; set; }
public string Name { get; set; }
public string ContactPerson { get; set; }
public string ComboText => $"{this.AgencyNumber}\t{this.Name}";
}
Ginger Ninja | Kelly | Diederik Krols definitely provide a nice all in one solution, but it may be a tad on the heavy side for simple use cases. For example, the derived ComboBox gets a reference to the internal editable textbox. As Diederik points out "We need this to get access to the Selection.". Which may not be a requirement at all. Instead we could simply bind to the Text property.
<ComboBox
ItemsSource="{Binding Agencies}"
SelectedItem="{Binding SelectedAgency}"
Text="{Binding SearchText}"
IsTextSearchEnabled="False"
DisplayMemberPath="ComboText"
IsEditable="True"
StaysOpenOnEdit="True"
MinWidth="200" />
Another possible improvement is to expose the filter, so devs could easily change it. Turns out this can all be accomplished from the viewmodel. To keep things interesting I chose to use the Agency's ComboText property for DisplayMemberPath, but its Name property for the custom filter. You could, of course, tweak this however you like.
public class MainViewModel : ViewModelBase
{
private readonly ObservableCollection<Agency> _agencies;
public MainViewModel()
{
_agencies = GetAgencies();
Agencies = (CollectionView)new CollectionViewSource { Source = _agencies }.View;
Agencies.Filter = DropDownFilter;
}
#region ComboBox
public CollectionView Agencies { get; }
private Agency selectedAgency;
public Agency SelectedAgency
{
get { return selectedAgency; }
set
{
if (value != null)
{
selectedAgency = value;
OnPropertyChanged();
SearchText = selectedAgency.ComboText;
}
}
}
private string searchText;
public string SearchText
{
get { return searchText; }
set
{
if (value != null)
{
searchText = value;
OnPropertyChanged();
if(searchText != SelectedAgency.ComboText) Agencies.Refresh();
}
}
}
private bool DropDownFilter(object item)
{
var agency = item as Agency;
if (agency == null) return false;
// No filter
if (string.IsNullOrEmpty(SearchText)) return true;
// Filtered prop here is Name != DisplayMemberPath ComboText
return agency.Name.ToLower().Contains(SearchText.ToLower());
}
#endregion ComboBox
private static ObservableCollection<Agency> GetAgencies()
{
var agencies = new ObservableCollection<Agency>
{
new Agency { AgencyNumber = 1, Name = "Foo", Title = "A" },
new Agency { AgencyNumber = 2, Name = "Bar", Title = "C" },
new Agency { AgencyNumber = 3, Name = "Elo", Title = "B" },
new Agency { AgencyNumber = 4, Name = "Baz", Title = "D" },
new Agency { AgencyNumber = 5, Name = "Hello", Title = "E" },
};
return agencies;
}
}
The main gotchas:
When the user enters a search and then selects an item from the filtered list, we want SearchText to be updated accordingly.
When this happens, we don't want to refresh the filter. For this demo, we're using a different property for DisplayMemberPath and our custom filter. So if we would let the filter refresh, the filtered list would be empty (no matches are found) and the selected item would be cleared as well.
On a final note, if you specify the ComboBox's ItemTemplate, you'll want to set TextSearch.TextPath instead of DisplayMemberPath.
If you refer to this answer
This should put you in the correct direction. It operated in the manner i believe you need when i tested it. For completeness ill add the code:
public class FilteredComboBox : ComboBox
{
private string oldFilter = string.Empty;
private string currentFilter = string.Empty;
protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null)
{
var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterItem;
}
if (oldValue != null)
{
var view = CollectionViewSource.GetDefaultView(oldValue);
if (view != null) view.Filter -= FilterItem;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Tab:
case Key.Enter:
IsDropDownOpen = false;
break;
case Key.Escape:
IsDropDownOpen = false;
SelectedIndex = -1;
Text = currentFilter;
break;
default:
if (e.Key == Key.Down) IsDropDownOpen = true;
base.OnPreviewKeyDown(e);
break;
}
// Cache text
oldFilter = Text;
}
protected override void OnKeyUp(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
case Key.Down:
break;
case Key.Tab:
case Key.Enter:
ClearFilter();
break;
default:
if (Text != oldFilter)
{
RefreshFilter();
IsDropDownOpen = true;
}
base.OnKeyUp(e);
currentFilter = Text;
break;
}
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
ClearFilter();
var temp = SelectedIndex;
SelectedIndex = -1;
Text = string.Empty;
SelectedIndex = temp;
base.OnPreviewLostKeyboardFocus(e);
}
private void RefreshFilter()
{
if (ItemsSource == null) return;
var view = CollectionViewSource.GetDefaultView(ItemsSource);
view.Refresh();
}
private void ClearFilter()
{
currentFilter = string.Empty;
RefreshFilter();
}
private bool FilterItem(object value)
{
if (value == null) return false;
if (Text.Length == 0) return true;
return value.ToString().ToLower().Contains(Text.ToLower());
}
}
The XAML I used to test:
<Window x:Class="CustomComboBox.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:CustomComboBox"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainWindowVM/>
</Window.DataContext>
<Grid>
<local:FilteredComboBox IsEditable="True" x:Name="MyThing" HorizontalAlignment="Center" VerticalAlignment="Center"
Height="25" Width="200"
ItemsSource="{Binding MyThings}"
IsTextSearchEnabled="True"
IsEnabled="True"
StaysOpenOnEdit="True">
</local:FilteredComboBox>
</Grid>
My ViewModel:
public class MainWindowVM : INotifyPropertyChanged
{
private ObservableCollection<string> _myThings;
public ObservableCollection<string> MyThings { get { return _myThings;} set { _myThings = value; RaisePropertyChanged(); } }
public MainWindowVM()
{
MyThings = new ObservableCollection<string>();
MyThings.Add("Hallo");
MyThings.Add("Jello");
MyThings.Add("Rollo");
MyThings.Add("Hella");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
If it doesnt meet your exact needs im sure you can edit it. Hope this helps.
Use the .Contains method.
This method will return true if the string contains the string you pass as a parameter.
Else it will return false.
if(agency.Title.Contains(combobox.Text))
{
//add this object to the List/Array that contains the object which will be shown in the combobox
}
Related
Going through:
WPF binding not updating the view
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?redirectedfrom=MSDN&view=netcore-3.1
and
WPF DataContext updated but UI not updated
I still can't see why the UI is not updating in the following case (my best guess is that the DataContext of the Grid to be updated is not updated) and am loosing my mind:
AppliedJobsModel.cs (has IPropertyChange implemented as some of the answers suggest):
public class AppliedJobsModel { }
public class AppliedJob : INotifyPropertyChanged
{
private string appliedDate;
private string url;
private string company;
private string description;
private string contact;
private string stack;
private string response;
private string interviewDate;
public AppliedJob(string[] entries)
{
appliedDate = entries[Consts.APPLIED_DATE_INDEX];
url = entries[Consts.URL_INDEX];
company = entries[Consts.COMPANY_INDEX];
description = entries[Consts.DESCRIPTION_INDEX];
contact = entries[Consts.CONTACT_INDEX];
stack = entries[Consts.STACK_INDEX];
response = entries[Consts.RESPONSE_INDEX];
interviewDate = entries[Consts.INTERVIEWDATE_INDEX];
}
public string AppliedDate
{
get {
return appliedDate;
}
set {
if (appliedDate != value)
{
appliedDate = value;
RaisePropertyChanged("AppliedDate");
}
}
}
public string Url
{
get
{
return url;
}
set
{
if (url != value)
{
url = value;
RaisePropertyChanged("Url");
}
}
}
public string Company
{
get
{
return company;
}
set
{
if (company != value)
{
company = value;
RaisePropertyChanged("Company");
}
}
}
public string Description
{
get
{
return description;
}
set
{
if (description != value)
{
description = value;
RaisePropertyChanged("Description");
}
}
}
public string Contact
{
get
{
return contact;
}
set
{
if (contact != value)
{
contact = value;
RaisePropertyChanged("Contact");
}
}
}
public string Stack
{
get
{
return stack;
}
set
{
if (stack != value)
{
stack = value;
RaisePropertyChanged("Stack");
}
}
}
public string Response
{
get
{
return response;
}
set
{
if (response != value)
{
response = value;
RaisePropertyChanged("Response");
}
}
}
public string InterviewDate
{
get
{
return interviewDate;
}
set
{
if (interviewDate != value)
{
interviewDate = value;
RaisePropertyChanged("InterviewDate");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
AppliedJobsViewModel.cs (has an observable collection that gets correctly updated when a button is clicked (in dbg)):
class AppliedJobsViewModel
{
private TexParser texParser;
public AppliedJobsViewModel() {
// TODO:
// -- do nothing here
}
public ObservableCollection<AppliedJob> AppliedJobsCollection
{
get;
set;
}
private ICommand _openTexClick;
public ICommand OpenTexClick
{
get
{
return _openTexClick ?? (_openTexClick = new CommandHandler(() => ReadAndParseTexFile(), () => CanExecute));
}
}
public bool CanExecute
{
get
{
// check if executing is allowed, i.e., validate, check if a process is running, etc.
return true;
}
}
public async Task ReadAndParseTexFile()
{
if (texParser == null)
{
texParser = new TexParser();
}
// Read file asynchronously here
await Task.Run(() => ReadFileAndUpdateUI());
}
private void ReadFileAndUpdateUI()
{
texParser.ReadTexFile();
string[][] appliedJobsArray = texParser.getCleanTable();
// Use this:
// https://rachel53461.wordpress.com/2011/09/17/wpf-grids-rowcolumn-count-properties/
// Update collection here
List<AppliedJob> appliedJobsList = createAppliedJobsListFromTable(appliedJobsArray);
AppliedJobsCollection = new ObservableCollection<AppliedJob>(appliedJobsList);
}
private List<AppliedJob> createAppliedJobsListFromTable(string[][] table)
{
List<AppliedJob> jobsList = new List<AppliedJob>();
for (int i = 0; i < table.Length; i++)
{
jobsList.Add(new AppliedJob(table[i]));
}
return jobsList;
}
}
AppliedJobsView.xaml:
<UserControl x:Class="JobTracker.Views.AppliedJobsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:JobTracker.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Name="appliedJobsGrid" Grid.Row="1" Grid.Column="1" Background="#50000000" Margin="10,10,10,10">
<ItemsControl ItemsSource = "{Binding Path = AppliedJobsCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = AppliedDate, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Url, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Company, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Description, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Contact, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Stack, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Response, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = InterviewDate, Mode = TwoWay}" Width = "100" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
TrackerHome.xaml (main page/uses the user control):
<Grid Grid.Row="1" Grid.Column="1">
<views:AppliedJobsView x:Name = "AppliedJobsControl" Loaded = "AppliedJobsViewControl_Loaded" />
</Grid>
TrackerHome.cs:
public TrackerHome()
{
InitializeComponent();
// Set data context here (https://stackoverflow.com/questions/12422945/how-to-bind-wpf-button-to-a-command-in-viewmodelbase)
// https://stackoverflow.com/questions/33929513/populate-a-datagrid-using-viewmodel-via-a-database
if (appliedJobsViewModel == null)
{
appliedJobsViewModel = new AppliedJobsViewModel();
}
this.DataContext = appliedJobsViewModel;
//AppliedJobControl.DataContext = appliedJobsViewModel;
}
private void AppliedJobsViewControl_Loaded(object sender, RoutedEventArgs e)
{
if (appliedJobsViewModel == null)
{
appliedJobsViewModel = new AppliedJobsViewModel();
}
AppliedJobsControl.DataContext = appliedJobsViewModel;
}
You are setting a new value of property here:
AppliedJobsCollection = new ObservableCollection<AppliedJob>(appliedJobsList);
but it's a simple auto-property without notification.
Make it full property (view model needs to implement INotifyPropertyChange):
ObservableCollection<AppliedJob> _appliedJobsCollection =
new ObservableCollection<AppliedJob>(); // empty initially
public ObservableCollection<AppliedJob> AppliedJobsCollection
{
get => _appliedJobsCollection;
set
{
_appliedJobsCollection = value;
RaisePropertyChanged(nameof(AppliedJobsCollection));
}
}
How does the full property behave? Is it as if all entries in each item in the collection have been changed (and thus have their properties changed)?
See this pseudo-code.
// given that AppliedJobsCollection is already initialized
// modify existing collection -> works
// bindings was subscribed to CollectionChanged event and will update
AppliedJobsCollection.Add(new AppliedJob(...));
// change item property -> works
// you implement INotifyPropertyChanged for items
// bindings was subscribed to that and will update
AppliedJobsCollection[0].Company = "bla";
// new instance of collection -> ... doesn't works
// how bindings can update?
AppliedJobsCollection = new ObservableCollection<AppliedJob>(...);
For last scenario to work you need to implement INotifyPropertyChanged for a class containing AppliedJobsCollection property and rise notification.
I have 3 combo boxes
<Grid>
<ComboBox Name="cbo1" SelectionChanged="OnComboBoxChanged" />
<ComboBox Name="cbo2" SelectionChanged="OnComboBoxChanged"/>
<ComboBox Name="cbo3" SelectionChanged="OnComboBoxChanged" />
The list for combo boxes is { a,b,c,d}
so if "b" is selected in the first box then the drop down should not have b and it will need to updated with {a,c,d} if the second one is set to a then last one need to have {c,d}. if they go back and change any we need to update the list accordingly. I addded a event oncomboboxchanged but it is not updating the combo box , when i set the item source to the new list.
private List<string> comboList = new List<string>();
string[] defaultParam = { "A", "B", "C", "D" };
public MainWindow()
{
InitializeComponent();
foreach(string s in defaultParam)
{
LoadCombo(s);
}
}
public void LoadCombo(string name)
{
comboList.Add(name);
cbo1.ItemsSource = comboList;
cbo2.ItemsSource = comboList;
cbo3.ItemsSource = comboList;
}
private void OnComboBoxChanged(object sender,SelectionChangedEventArgs e)
{
var combo = sender as ComboBox;
string oldval = combo.Text;
string id = combo.Name;
string itemSel = (sender as ComboBox).SelectedItem.ToString();
comboList.Remove(itemSel);
//add old value only if it is not empty
if (!string.IsNullOrEmpty(oldval))
{
comboList.Add(oldval);
}
combo.ItemsSource = comboList;
ComboBox[] comboNameLst = {cbo1,cbo2,cbo3 };
foreach (ComboBox cbo in comboNameLst)
{
if (id != cbo.Name)
{
if (cbo.SelectedItem == null)
{
cbo.ItemsSource = comboList;
}
else if (cbo.SelectedItem != null)
{
string tempitemsel = cbo.SelectedItem.ToString();
comboList.Add(tempitemsel);
cbo.ItemsSource = comboList;
comboList.Remove(tempitemsel);
}
}
}
}
so cbo.ItemSource is not doing any thing , do I need to do any thing differently so I see the update.
You need to use binding in XAML, rather than set ItemsSource in your code behind. Also data bind SelectedItem:
<Grid>
<ComboBox ItemsSource="{Binding DefaultList}" SelectedItem="{Binding SelectedItem_Cob1}"/>
<ComboBox ItemsSource="{Binding FilteredListA}" SelectedItem="{Binding SelectedItem_Cob2}"/>
<ComboBox ItemsSource="{Binding FilteredListB}" SelectedItem="{Binding SelectedItem_Cob3}"/>
</Grid>
In your code behind, you need to implement INotifyPropertyChanged; define your relevant ItemsSources, and SlectedItems as properties; and set your Windows's DataContext to your code itself (you should use MVVM pattern but you could worry about that later) :
using System.ComponentModel;
public partial class MainWindow: INotifyPropertyChanged
{
string[] defaultParam = { "A", "B", "C", "D" };
private string _selecteditem_cob1;
private string _selecteditem_cob2;
private string _selecteditem_cob3;
public List<string> DefaultList
{
get { return defaultParam.ToList(); }
}
public string SelectedItem_Cob1
{
get { return _selecteditem_cob1; }
set
{
if (_selecteditem_cob1 != value)
{
_selecteditem_cob1 = value;
RaisePropertyChanged("SelectedItem_Cob1");
RaisePropertyChanged("FilteredListA");
RaisePropertyChanged("FilteredListB");
}
}
}
public string SelectedItem_Cob2
{
get { return _selecteditem_cob2; }
set
{
if (_selecteditem_cob2 != value)
{
_selecteditem_cob2 = value;
RaisePropertyChanged("SelectedItem_Cob2");
RaisePropertyChanged("FilteredListB");
}
}
}
public string SelectedItem_Cob3
{
get { return _selecteditem_cob3; }
set
{
if (_selecteditem_cob3 != value)
{
_selecteditem_cob3 = value;
RaisePropertyChanged("SelectedItem_Cob3");
}
}
}
public List<string> FilteredListA
{
get { return defaultParam.ToList().Where(a=>a!=SelectedItem_Cob1).ToList(); }
}
public List<string> FilteredListB
{
get { return FilteredListA.Where(a => a != SelectedItem_Cob2).ToList(); }
}
public MainWindow()
{
InitializeComponent();
this.DataContext=this;
}
//Implementation for INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
protected void RaisePropertyChanged(String propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
Result:
Three ComboBoxes will all show A,B,C,D at the initial stage. And
then if user made selections cbo2 and cbo3 will only display
filtered result dynamically.
I realized this is not 100% what you want (thanks to #TheodosiusVonRichthofen), but I feel you can still use this, and be able to easily modify it to suit your needs.
Also, the list that contains the combo-box items should be an ObservableCollection instead of a List. By making it an ObservableCollection, the combo-box items will be updated when you add/remove/change items in the lists.
I am using ComboBox ItemsSource property binding to display items from a List to combo box.
Following is the code:
<ComboBox x:Name="Cmb_Tax" ItemsSource="{Binding TaxList}"
DisplayMemberPath="ChargeName" SelectedItem="{Binding
SelectedTax,UpdateSourceTrigger=PropertyChanged}" IsEditable="True"
IsTextSearchEnabled="True" SelectionChanged="Cmb_Tax_SelectionChanged"/>
Classes.Charges _selected_tax = new Classes.Charges();
public Classes.Charges SelectedTax
{
get
{
return _selected_tax;
}
set
{
_selected_tax = value;
}
}
List<Classes.Charges> _taxlist = new List<Classes.Charges>();
public List<Classes.Charges> TaxList
{
get
{
return _taxlist;
}
set
{
_taxlist = value;
OnPropertyChanged("TaxList");
}
}
It displays the items in the combo box correctly.
There is a particular item in TaxList "No Tax" which I want to be selected by default in the combo box. This item can be present at any index in the list (Not necessary first or last item of the list).
I am trying to use the following code to set the selected index property of combo box, but sadly its not working.
TaxList = Classes.Charges.GetChargeList("Tax");
Cmb_Tax.DataContext = this;
int i = TaxList.FindIndex(x => x.ChargeName == tax_name);
Cmb_Tax.SelectedIndex = i;
The Method FindIndex() returns the index of the "No Tax" correctly but when I try assigning it to SelectedIndex of combo the SelectedIndex doesn't change. It stays at -1.
Update1
private void Cmb_Tax_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
MessageBox.Show(SelectedTax.ChargeName);
}
Update2
Updated the code as per suggested by #ElectricRouge
<ComboBox x:Name="Cmb_Tax" ItemsSource="{Binding TaxList, Mode=TwoWay}"
DisplayMemberPath="ChargeName" SelectedItem="{Binding SelectedTax,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}"
IsEditable="True" IsTextSearchEnabled="True"
SelectionChanged="Cmb_Tax_SelectionChanged"/>
Classes.Charges _selected_tax = new Classes.Charges();
public Classes.Charges SelectedTax
{
get
{
return _selected_tax;
}
set
{
_selected_tax = value;
OnPropertyChanged("SelectedTax");
}
}
ObservableCollection<Classes.Charges> _taxlist = new ObservableCollection<Classes.Charges>();
public ObservableCollection<Classes.Charges> TaxList
{
get
{
return _taxlist;
}
set
{
_taxlist = value;
OnPropertyChanged("TaxList");
}
}
public void Load_Tax(string tax_name = null, Classes.Charges selected_tax = null)
{
TaxList = Classes.Charges.GetParticularChargeList("Tax");
Cmb_Tax.DataContext = this;
//Cmb_Tax.SelectedValue = tax_name;
SelectedTax = selected_tax;
//int i = TaxList.FindIndex(x => x.ChargeName == tax_name);
//Cmb_Tax.SelectedIndex = i;
}
Any idea why this must be happening?
Also please suggest any other approach to display default in combo box.
Here's a working sample:
Viewmodel:
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
this.DataContext = vm;
this.Loaded += (o,e) => vm.LoadData();
}
public class ViewModel : INotifyPropertyChanged
{
private IList<Charges> taxList;
public ICollectionView TaxList { get; private set; }
public void LoadData()
{
taxList = Charges.GetChargeList("taxes");
TaxList = CollectionViewSource.GetDefaultView(taxList);
RaisePropertyChanged("TaxList");
TaxList.CurrentChanged += TaxList_CurrentChanged;
var noTax = taxList.FirstOrDefault(c => c.ChargeName == "No Tax");
TaxList.MoveCurrentTo(noTax);
}
void TaxList_CurrentChanged(object sender, EventArgs e)
{
var currentCharge = TaxList.CurrentItem as Charges;
if(currentCharge != null)
MessageBox.Show(currentCharge.ChargeName);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
View:
<ComboBox x:Name="cboTaxList"
ItemsSource="{Binding TaxList}"
DisplayMemberPath="ChargeName"
IsSynchronizedWithCurrentItem="True" />
List does not implement INotifyCollectionChanged make it ObservableCollection
ObservableCollection<Classes.Charges> _taxlist = new ObservableCollection<Classes.Charges>();
public ObservableCollection<Classes.Charges> TaxList
{
get
{
return _taxlist;
}
set
{
_taxlist = value;
OnPropertyChanged("TaxList");
}
}
And try setting the Mode=TwoWay
SelectedItem="{Binding SelectedTax,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}"
I have 2 combo boxes, one that contains a list of 'Items' and another that contains a list of 'Subitems'.
The list of Subitems depends on the currently selected Item.
I've got most of this working (by binding the ItemSource of the Subitems to a PossibleSubitems property), however the problem is when I change the Item and the Subitem is no longer valid for the new item. In this case I just want to pick the first valid subitem, but instead I get a blank combo-box. Note that I think that the property in the class is set correctly, but the binding doesn't seem to reflect it correctly.
Here's some code to show you what I'm doing. In this case, I have:
'Item 1' which can have SubItem A or Subitem B and
'Item 2' which can have SubItem B or Subitem C
The problem comes when I switch to Item 2 when I have Subitem A selected.
XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="134" Width="136">
<StackPanel Height="Auto" Width="Auto">
<ComboBox ItemsSource="{Binding PossibleItems, Mode=OneWay}" Text="{Binding CurrentItem}"/>
<ComboBox ItemsSource="{Binding PossibleSubitems, Mode=OneWay}" Text="{Binding CurrentSubitem}"/>
</StackPanel>
</Window>
Code Behind:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
namespace WpfApplication1
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
// List of potential Items, used to populate the options for the Items combo box
public ObservableCollection<string> PossibleItems
{
get
{
ObservableCollection<string> retVal = new ObservableCollection<string>();
retVal.Add("Item 1");
retVal.Add("Item 2");
return retVal;
}
}
// List of potential Items, used to populate the options for the Subitems combo box
public ObservableCollection<string> PossibleSubitems
{
get
{
ObservableCollection<string> retVal = new ObservableCollection<string>();
if (CurrentItem == PossibleItems[0])
{
retVal.Add("Subitem A");
retVal.Add("Subitem B");
}
else
{
retVal.Add("Subitem B");
retVal.Add("Subitem C");
}
return retVal;
}
}
// Track the selected Item
private string _currentItem;
public string CurrentItem
{
get { return _currentItem; }
set
{
_currentItem = value;
// Changing the item changes the possible sub items
NotifyPropertyChanged("PossibleSubitems");
}
}
// Track the selected Subitem
private string _currentSubitem;
public string CurrentSubitem
{
get { return _currentSubitem; }
set
{
if (PossibleSubitems.Contains(value))
{
_currentSubitem = value;
}
else
{
_currentSubitem = PossibleSubitems[0];
// We're not using the valuie specified, so notify that we have in fact changed
NotifyPropertyChanged("CurrentSubitem");
}
}
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
CurrentItem = PossibleItems[0];
CurrentSubitem = PossibleSubitems[0];
}
public event PropertyChangedEventHandler PropertyChanged;
internal void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
I rewrote my own sample - kept it code behind so as not to deviate from your sample too much. Also, I'm using .NET 4.5 so didn't have to provide property names in OnPropertyChanged calls - you will need to insert them if on .NET 4.0. This works in all scenarios.
In practice, I'd recommend locating this code in a view-model as per the MVVM pattern. Aside from the binding of the DataContext, It wouldn't look too different from this implemenation though.
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
namespace WpfApplication1
{
public partial class MainWindow: Window, INotifyPropertyChanged
{
private string _currentItem;
private string _currentSubitem;
private ObservableCollection<string> _possibleItems;
private ObservableCollection<string> _possibleSubitems;
public MainWindow()
{
InitializeComponent();
LoadPossibleItems();
CurrentItem = PossibleItems[0];
UpdatePossibleSubItems();
DataContext = this;
CurrentItem = PossibleItems[0];
CurrentSubitem = PossibleSubitems[0];
PropertyChanged += (s, o) =>
{
if (o.PropertyName != "CurrentItem") return;
UpdatePossibleSubItems();
ValidateCurrentSubItem();
};
}
private void ValidateCurrentSubItem()
{
if (!PossibleSubitems.Contains(CurrentSubitem))
{
CurrentSubitem = PossibleSubitems[0];
}
}
public ObservableCollection<string> PossibleItems
{
get { return _possibleItems; }
private set
{
if (Equals(value, _possibleItems)) return;
_possibleItems = value;
OnPropertyChanged();
}
}
public ObservableCollection<string> PossibleSubitems
{
get { return _possibleSubitems; }
private set
{
if (Equals(value, _possibleSubitems)) return;
_possibleSubitems = value;
OnPropertyChanged();
}
}
public string CurrentItem
{
get { return _currentItem; }
private set
{
if (value == _currentItem) return;
_currentItem = value;
OnPropertyChanged();
}
}
public string CurrentSubitem
{
get { return _currentSubitem; }
set
{
if (value == _currentSubitem) return;
_currentSubitem = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void LoadPossibleItems()
{
PossibleItems = new ObservableCollection<string>
{
"Item 1",
"Item 2"
};
}
private void UpdatePossibleSubItems()
{
if (CurrentItem == PossibleItems[0])
{
PossibleSubitems = new ObservableCollection<string>
{
"Subitem A",
"Subitem B"
};
}
else if (CurrentItem == PossibleItems[1])
{
PossibleSubitems = new ObservableCollection<string>
{
"Subitem B",
"Subitem C"
};
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You're notifying the wrong property. On your CurrentItem, you call the "PossibleSubitems".
private string _currentItem;
public string CurrentItem
{
get { return _currentItem; }
set
{
_currentItem = value;
// Changing the item changes the possible sub items
NotifyPropertyChanged("PossibleSubitems");
}
}
Fix that and try again :)
WARNING ... THIS IS A HACK ...
I changed this to make it work (just because I was curious), but this is by no mean the proper way, nor an elegant one:
// List of potential Items, used to populate the options for the Subitems combo box
public ObservableCollection<string> PossibleSubitems { get; set; }
// Track the selected Item
private string _currentItem;
public string CurrentItem
{
get { return _currentItem; }
set
{
_currentItem = value;
// Changing the item changes the possible sub items
if (value == "Item 1")
PossibleSubitems = new ObservableCollection<string>() {"A","B"} ;
else
PossibleSubitems = new ObservableCollection<string>() { "C", "D" };
RaisePropertyChanged("CurrentItem");
RaisePropertyChanged("PossibleSubitems");
}
}
So basically, when current item change, it'll create new collection of subitems ...
UGLY !!! I know ... You could reuse those collections, and do lots of other things ... but as I said, I was curious about if it can be done this way ... :)
If this breaks your keyboard, or your cat runs away, I TAKE NO RESPONSIBILITY WHATSOEVER.
I am trying to use the Silverlight 3.0 DataGrid with the MVVM design pattern. My page has a DataGrid and a button that adds an item to the collection in the VM using a command (from the Composite Application Library). This works fine, and the new item is displayed and selected.
The problem I can't solve is how to begin editing the row. I want the new row to be immediately editable when the user clicks the Add button i.e. focus set to the DataGrid and the new row in edit mode.
This is the XAML in the view:
<Grid x:Name="LayoutRoot">
<StackPanel>
<data:DataGrid ItemsSource="{Binding DataView}"/>
<Button cmd:Click.Command="{Binding AddItemCommand}" Content="Add" />
</StackPanel>
</Grid>
The code behind has one line of code that creates an instance of the VM and sets the DataContext of the view.
The VM code is:
public class VM
{
public List<TestData> UnderlyingData { get; set; }
public PagedCollectionView DataView { get; set; }
public ICommand AddItemCommand { get; set; }
public VM()
{
AddItemCommand = new DelegateCommand<object>(o =>
{
DataView.AddNew();
});
UnderlyingData = new List<TestData>();
UnderlyingData.Add(new TestData() { Value = "Test" });
DataView = new PagedCollectionView(UnderlyingData);
}
}
public class TestData
{
public string Value { get; set; }
public TestData()
{
Value = "<new>";
}
public override string ToString()
{
return Value.ToString();
}
}
What would be the best way to solve this problem using the MVVM design pattern?
I faced the same issue. I've introduced interface ISupportEditingState:
public interface ISupportEditingState
{
EditingState EditingState { get; set; }
}
My VM implements it. And then I wrote this behaviour to synchronise editing state of DataGrid and my VM:
public class SynchroniseDataGridEditingStateBehaviour : Behavior<DataGrid>
{
public static readonly DependencyProperty EditingStateBindingProperty =
DependencyProperty.Register("EditingStateBinding", typeof(ISupportEditingState),
typeof(SynchroniseDataGridEditingStateBehaviour), new PropertyMetadata(OnEditingStateBindingPropertyChange));
private bool _attached;
private bool _changingEditingState;
public ISupportEditingState EditingStateBinding
{
get { return (ISupportEditingState)GetValue(EditingStateBindingProperty); }
set { SetValue(EditingStateBindingProperty, value); }
}
private static void OnEditingStateBindingPropertyChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var b = d as SynchroniseDataGridEditingStateBehaviour;
if (b == null)
return;
var oldNotifyChanged = e.OldValue as INotifyPropertyChanged;
if (oldNotifyChanged != null)
oldNotifyChanged.PropertyChanged -= b.OnEditingStatePropertyChanged;
var newNotifyChanged = e.NewValue as INotifyPropertyChanged;
if (newNotifyChanged != null)
newNotifyChanged.PropertyChanged += b.OnEditingStatePropertyChanged;
var newEditingStateSource = e.NewValue as ISupportEditingState;
if (newEditingStateSource.EditingState == EditingState.Editing)
{
// todo: mh: decide on this behaviour once again.
// maybe it's better to start editing if selected item is already bound in the DataGrid
newEditingStateSource.EditingState = EditingState.LastCancelled;
}
}
private static readonly string EditingStatePropertyName =
CodeUtils.GetPropertyNameByLambda<ISupportEditingState>(ses => ses.EditingState);
private void OnEditingStatePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_changingEditingState || !_attached || e.PropertyName != EditingStatePropertyName)
return;
_changingEditingState = true;
var editingStateSource = sender as ISupportEditingState;
if (editingStateSource == null)
return;
var grid = AssociatedObject;
var editingState = editingStateSource.EditingState;
switch (editingState)
{
case EditingState.Editing:
grid.BeginEdit();
break;
case EditingState.LastCancelled:
grid.CancelEdit();
break;
case EditingState.LastCommitted:
grid.CommitEdit();
break;
default:
throw new InvalidOperationException("Provided EditingState is not supported by the behaviour.");
}
_changingEditingState = false;
}
protected override void OnAttached()
{
var grid = AssociatedObject;
grid.BeginningEdit += OnBeginningEdit;
grid.RowEditEnded += OnEditEnded;
_attached = true;
}
protected override void OnDetaching()
{
var grid = AssociatedObject;
grid.BeginningEdit -= OnBeginningEdit;
grid.RowEditEnded -= OnEditEnded;
_attached = false;
}
void OnEditEnded(object sender, DataGridRowEditEndedEventArgs e)
{
if (_changingEditingState)
return;
EditingState editingState;
if (e.EditAction == DataGridEditAction.Commit)
editingState = EditingState.LastCommitted;
else if (e.EditAction == DataGridEditAction.Cancel)
editingState = EditingState.LastCancelled;
else
return; // if DataGridEditAction will ever be extended, this part must be changed
EditingStateBinding.EditingState = editingState;
}
void OnBeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
if (_changingEditingState)
return;
EditingStateBinding.EditingState = EditingState.Editing;
}
}
Works ok for me, hope it helps.
Whenever you talk about directly accessing ui components, your kinda missing the point of mvvm. The ui binds to the viewmodel, so find a way to alter the viewmodel instead.