WPF ObservableCollection not updating in ribbon view - c#

I've created a C# WPF app with a RibbonApplicationMenu displaying a Most Recently Used (MRU) list. Unfortunately the display doesn't update when I select an existing file from the list or upload a new file. In the XAML I have:
<local:MostRecentFiles x:Key="MostRecentFilesData" />
...
<ribbon:RibbonApplicationMenu.AuxiliaryPaneContent>
<ribbon:RibbonGallery Name="RecentDocuments" CanUserFilter="False"
SelectedValue="{Binding MostRecentFile, UpdateSourceTrigger=PropertyChanged}">
<ribbon:RibbonGalleryCategory Header="Recent Documents"
ItemsSource="{DynamicResource MostRecentFilesData}">
</ribbon:RibbonGalleryCategory>
</ribbon:RibbonGallery>
</ribbon:RibbonApplicationMenu.AuxiliaryPaneContent>
The DataContext is set to a class containing
private ObservableCollection<string> _mostRecentFile = new ObservableCollection<string>();
public ObservableCollection<string> MostRecentFile
{
get { return _mostRecentFile; }
set
{
_mostRecentFile = value;
OnPropertyChanged("MostRecentFile");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
In the OpenFile routine the code is
MostRecentFiles mrf = new MostRecentFiles();
mrf.AddMRUitem(openFileDlg.FileName);
The MostRecentFiles class contains the main class methods and I've put some sample file paths in the code.
public class MostRecentFiles : ObservableCollection<string>
{
public ObservableCollection<string> MRUmenuItems = new ObservableCollection<string>();
public MostRecentFiles()
{
AddMRUitem(#"C:\MyDocuments\File3.txt"); //
AddMRUitem(#"C:\MyDocuments\File2.txt"); // } Sample files
AddMRUitem(#"C:\MyDocuments\File1.txt"); //
}
public void AddMRUitem(string filePath)
{
int result;
result = MRUmenuItems.IndexOf(filePath);
if (result != -1)
{
MRUmenuItems.Remove(filePath);
MRUmenuItems.Insert(0, filePath);
}
else
AddMenuItem(filePath);
UpdateMRUList();
}
private void UpdateMRUList()
{
this.Clear();
foreach (string filePath in MRUmenuItems)
{
this.Add(filePath);
}
//OnPropertyChanged("MostRecentFile"); // <= Error CS1503
}
private void AddMenuItem(string newMRUfile)
{
MRUmenuItems.Insert(0, newMRUfile);
if (MRUmenuItems.Count > 10)
{
MRUmenuItems.RemoveAt(MRUmenuItems.Count - 1);
}
}
private string _mostRecentFile = "";
public string MostRecentFile
{
get { return _mostRecentFile; }
set
{
if (_mostRecentFile == value) return;
_mostRecentFile = value;
AddMRUitem(_mostRecentFile);
//OnPropertyChanged("MostRecentFile");
}
}
}
Undeleting OnPropertyChanged in UpdateMRUList() produces the error: Error CS1503 Argument 1: cannot convert from 'string' to 'System.ComponentModel.PropertyChangedEventArgs'
When I launch the program the menu correctly displays the three files but when I select one the displayed order doesn't change; I expect the selected file to move to the top of the list. Similarly when I open a new file the filename isn't added to the MRU.
However if I step through the code the lists are being updated in the correct order. What have I done wrong?

You are binding SelectedValue to a collection. You don't need a custom collection. Just add an ObservableCollection to your view model and move items on selected item changed:
View model:
private void OnSelectedMostRecentFileChanged()
{
// Move the selected item to the front of the list
this.MostRecentFiles.Move(this.MostRecentFiles.IndexOf(this.SelectedRecentFile), 0);
}
private string _selectedRecentFile;
public string SelectedRecentFile
{
get { return _selectedRecentFile; }
set
{
_selectedRecentFile= value;
OnSelectedMostRecentFileChanged();
OnPropertyChanged(nameof(SelectedRecentFile));
}
}
private ObservableCollection<string> _mostRecentFiles = new ObservableCollection<string>();
public ObservableCollection<string> MostRecentFiles
{
get { return _mostRecentFiles; }
set
{
_mostRecentFiles = value;
OnPropertyChanged(nameof(MostRecentFiles));
}
}
View:
<ribbon:RibbonApplicationMenu.AuxiliaryPaneContent>
<ribbon:RibbonGallery Name="RecentDocuments" CanUserFilter="False"
SelectedItem="{Binding SelectedRecentFile}">
<ribbon:RibbonGalleryCategory Header="Recent Documents"
ItemsSource="{Binding MostRecentFiles}">
</ribbon:RibbonGalleryCategory>
</ribbon:RibbonGallery>
</ribbon:RibbonApplicationMenu.AuxiliaryPaneContent>

Related

WinUI x:Bind to Model property not updating UI

This is the first time I have used WinUI. I have all of the information I need pulling in, but the binding back to the UI to display the data after async functions isn't updating the UI controls.
I have a view model that is handling the streams to load in the string from the user's selected file, and storing that List<string> in the model object. The model object implements INotifyPropertyChanged. I must not understanding completely the relationship between x:bind and INotifyPropertyChanged because nothing is set from the view to the PropertyChanged event.
public class PCCBill : INotifyPropertyChanged
{
private string importFileName;
private List<string> lines;
private string originalFileText;
//private List<string> formattedLines;
public PCCBill(string displayFileName)
{
this.ImportFileName = displayFileName;
}
public string ImportFileName
{
get { return importFileName; }
set
{
this.importFileName = value;
RaisePropertyChanged(nameof(ImportFileName));
}
}
public List<string> Lines
{
get { return lines; }
set
{
this.lines = value;
string txt = "";
foreach (string line in this.lines)
{
txt += line + Environment.NewLine;
}
this.OriginalFileText = txt;
}
}
public string OriginalFileText
{
get { return originalFileText; }
set
{
this.originalFileText = value;
RaisePropertyChanged(nameof(OriginalFileText));
}
}
/*public string FormattedFileText
{
get
{
string txt = "";
foreach(string line in this.formattedLines)
{
txt += line + Environment.NewLine;
}
return txt;
}
}*/
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
I have the x:Bind set in the View with OneWay Mode:
<TextBlock Grid.Row="1" x:Name="FileNameTxtBlock" VerticalAlignment="Center"
Margin="15,0" Text="{x:Bind ViewModel.Bill.ImportFileName, Mode=OneWay}"/>
<TextBlock x:Name="OrgPreviewTxtBlock" Text="{x:Bind ViewModel.Bill.OriginalFileText, Mode=OneWay}"/>

Binding to WPF Combo Box

I have a model class that I wish to bind a combo box to. My plan was to have an object with two propertied. 1) an ObservableCollection that contains the items I want to populate the combo box with. 2) A string property that stores the value of the selected item. I cannot seem to get this to work and open to suggestions. I am Trying to follow MVVM as best as possible. The behavior I observe is an empty combo box.
The class looks like this.
public class WellListGroup : Notifier
{
private ObservableCollection<string> _headers;
public ObservableCollection<string> headers
{
get { return this._headers; }
set { this._headers = value; OnPropertyChanged("headers"); }
}
private string _selected;
public string selected
{
get { return this._selected;}
set { this._selected = value; OnPropertyChanged("selected");}
}
}
Notifier looks like:
public class Notifier : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
And my viewmodel makes a call to a data access layer that creates the following object i wish to bind to.
public class MainViewModel : Notifier
{
public static getWells gw = new getWells();
public static ObservableCollection<string> headers = gw.getHeaders();
public WellListGroup wlg = new WellListGroup {headers = headers, selected = null};
}
Data Access Layer - getHeaders()
public ObservableCollection<string> getHeaders()
{
ObservableCollection<string> vals = new ObservableCollection<string>();
WVWellModel wvm = new WVWellModel();
var properties = getProperties(wvm);
foreach (var p in properties)
{
string name = p.Name;
vals.Add(name);
}
return vals;
}
Then the view:
<ComboBox DockPanel.Dock="Top" ItemsSource = "{Binding Path = wlg.headers}" SelectedItem="{Binding Path = wlg.selected}"></ComboBox>
View Code Behind (Where the Data Context is set)
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
MainViewModel mvm = new MainViewModel();
DataContext = mvm;
}
}
App.xaml.cs
public partial class App : Application
{
private void OnStartup(object sender, StartupEventArgs e)
{
Views.MainView view = new Views.MainView();
view.Show();
}
private void APP_DispatcherUnhandledException(object sender,DispatcherUnhandledExceptionEventArgs e)
{
MessageBox.Show(e.Exception.Message);
e.Handled = true;
}
}
I have tried several iterations of this but cant for the life of me get this to work. I am presented with an empty combo box.
I am going to assume DataContext is set to MainViewModel on the view.
I think you well list group should call OnPropertyChanged
public class MainViewModel : Notifier
{
public static getWells gw = new getWells();
public static ObservableCollection<string> headers = gw.getHeaders();
private WellListGroup _wlg = new WellListGroup {headers = headers, selected = null};
public WellListGroup wlg
{
get { return _wlg; }
set { _wlg = value; OnPropertyChanged("wlg"); }
}
The combo box binding should look like this:
<ComboBox
ItemsSource = "{Binding wlg.headers}"
SelectedItem = "{Binding wlg.selected Mode=TwoWay}"
/>
If neither of those work I would make sure the MainViewModel is being instantiated and assigned to DataContext in the Page constructor or a page loaded event.
Here is a code project Tutorial that may help break down the binding process Step by Step WPF Data Binding with Comboboxes

WPF returning a List to the class

New to WPF and C# from VB web forms, so sorry for this poorly structured question I will add to as needed to improve. I am trying to implement an example by adding database calls to MySQL to populate an On-Demand Tree View control. Here is the link to the sample code...
sample code
Got my db connection working and data is populating my dataset. I iterate to place in a List. But can not seem to figure out the issue with passing the List to the Class to populate the control...
public class Level1
{
public Level1(string level1Name)
{
this.Level1Name = level1Name;
}
public string Level1Name { get; private set; }
readonly List<Level2> _level2s = new List<Level2>();
public List<Level2> Level2s
{
get { return _level2s; }
}
}
I have a database class that queries the db and parses the data....
List<string> level1s = new List<string>();
DataSet ds = new DataSet();
foreach (DataTable table in ds.Tables)
{
foreach (DataRow row in table.Rows)
{
level1s.Add((string)row["name"]);
}
}
**UPDATE**: Trying to return the list...
return new Level1[]
{
foreach(DataRow row in level1s)
{
// iterate here
}
};
My level1s List is properly populated, I am just drawing a blank on returning the values.
thanks,
UPDATE - I am including the ViewModel code here as well....
using BusinessLib;
namespace TreeViewWithViewModelTOC.LoadOnDemand
{
public class Level1ViewModel : TreeViewItemViewModel
{
readonly Level1 _level1;
public Level1ViewModel(Level1 level1)
: base(null, true)
{
_level1 = level1;
}
public string Level1Name
{
get { return _level1.Level1Name; }
}
protected override void LoadChildren()
{
foreach (Level2 level2 in Database.GetLevel2s(_level1))
base.Children.Add(new Level2ViewModel(level2, this));
}
}
}
Try like this below,
List<Level1> L1=new List<Level1>();
foreach(var row in level1s)
{
Level1 L=new Level1();
// L.Level1Name = row.ToString(); here add items as you need
L1.Add(L);
}
return L1.ToArray();
You should be using MVVM design pattern to solve this. There aren't many requirements listed in your questions so I will assume my own, which should lead you along the right path.
First thing is determining whether or not you're records are going to be ready/pulled at run-time--before the TreeView is rendered and if they will be changed/updated/added/removed from the structure during the lifecycle of the application. If the structure isn't going to be changed, you can continue to use List as your collection. If you're (or a user is) going to be adding/removing from the collection, ultimately changing the structure, then you need to notify the UI that a change occurred on the collection; so you would use the built in ObservableCollection for that. Here is a MVVM-purist solution, with the assumption that your data will be pulled at application startup and you will be modifying the collection:
Note: RelayCommand implementation was taken from here
Models
public class First
{
public string Name
{
get;
set;
}
public readonly List<Second> Children;
public First(string name)
{
Name = name;
Children = new List<Second>
{
new Second(1),
new Second(2),
new Second(3),
};
}
public void AddChild(Second child)
{
Children.Add(child);
ChildAdded(this, new ChildAddedEventArgs(child));
}
public EventHandler<ChildAddedEventArgs> ChildAdded;
}
public class ChildAddedEventArgs //technically, not considered a model
{
public readonly Second ChildAdded;
public ChildAddedEventArgs(Second childAdded)
{
ChildAdded = childAdded;
}
}
public class Second
{
public int Number
{
get;
set;
}
public Second(int number)
{
Number = number;
}
}
ViewModels
public class MainViewModel : INotifyPropertyChanged
{
private readonly ObservableCollection<FirstViewModel> _items;
private readonly ICommand _addFirstFirstChildCommand;
private readonly ICommand _addSecondFirstChildCommand;
private readonly ICommand _toggleExpandCollapseCommand;
private bool _firstAddedFlag;
public MainViewModel(IEnumerable<First> records)
{
_items = new ObservableCollection<FirstViewModel>();
foreach(var r in records)
{
_items.Add(new FirstViewModel(r));
}
_addFirstFirstChildCommand = new RelayCommand(param => AddFirst(), param => CanAddFirst);
_addSecondFirstChildCommand = new RelayCommand(param => AddSecond(), param => CanAddSecond);
_toggleExpandCollapseCommand = new RelayCommand(param => ExpandCollapseAll(), param =>
{
return true;
});
}
public ObservableCollection<FirstViewModel> Items
{
get
{
return _items;
}
}
public ICommand AddFirstFirstChildCommand
{
get
{
return _addFirstFirstChildCommand;
}
}
public ICommand AddSecondFirstChildCommand
{
get
{
return _addSecondFirstChildCommand;
}
}
public ICommand ToggleExpandCollapseCommand
{
get
{
return _toggleExpandCollapseCommand;
}
}
public bool CanAddFirst
{
get
{
return true;
}
}
public bool CanAddSecond
{
get
{
//Only allow second to be added if we added to first, first
return _firstAddedFlag;
}
}
public void AddFirstChild(FirstViewModel item)
{
Items.Add(item);
}
private void AddFirst()
{
_items[0].AddChild(new Second(10));
_firstAddedFlag = true;
}
private void AddSecond()
{
_items[1].AddChild(new Second(20));
}
private void ExpandCollapseAll()
{
foreach(var i in Items)
{
i.IsExpanded = !i.IsExpanded;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class FirstViewModel : INotifyPropertyChanged
{
private readonly First model;
private readonly ObservableCollection<SecondViewModel> _children;
private bool _isExpanded;
public FirstViewModel(First first)
{
_children = new ObservableCollection<SecondViewModel>();
model = first;
foreach(var s in first.Children)
{
Children.Add(new SecondViewModel(s));
}
model.ChildAdded += OnChildAdded;
}
public string FirstName
{
get
{
return model.Name;
}
set
{
model.Name = value;
NotifyPropertyChanged();
}
}
public ObservableCollection<SecondViewModel> Children
{
get
{
return _children;
}
}
public bool IsExpanded
{
get
{
return _isExpanded;
}
set
{
_isExpanded = value;
NotifyPropertyChanged();
}
}
internal void AddChild(Second second)
{
model.AddChild(second);
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnChildAdded(object sender, ChildAddedEventArgs args)
{
if(Children != null)
{
Children.Add(new SecondViewModel(args.ChildAdded));
}
}
}
public class SecondViewModel : INotifyPropertyChanged
{
private readonly Second model;
private bool _isExpanded;
public SecondViewModel(Second second)
{
model = second;
}
public int SecondNumber
{
get
{
return model.Number;
}
set
{
model.Number = value;
NotifyPropertyChanged();
}
}
//Added property to avoid warnings in output window
public bool IsExpanded
{
get
{
return _isExpanded;
}
set
{
_isExpanded = value;
NotifyPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Model Provider
public class Database
{
public static IEnumerable<First> GetChildren()
{
List<First> firsts = new List<First>();
firsts.Add(new First("John"));
firsts.Add(new First("Roxanne"));
return firsts;
}
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private MainViewModel mvm;
public MainWindow()
{
var db = Database.GetChildren();
mvm = new MainViewModel(db);
InitializeComponent();
DataContext = mvm;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
//Do not do this, example only
var f = new First("Billy");
mvm.AddFirstChild(new FirstViewModel(f));
//Prove that the event was raised in First, FirstViewModel see & handles it, and
//the UI is updated
f.AddChild(new Second(int.MaxValue));
}
}
MainWindow.xaml
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow">
<Grid>
<TreeView ItemsSource="{Binding Items}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:FirstViewModel}"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding FirstName}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:SecondViewModel}">
<TextBlock Text="{Binding SecondNumber}" />
</DataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded"
Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
<StackPanel Orientation="Vertical"
VerticalAlignment="Bottom">
<StackPanel Orientation="Horizontal">
<Button Content="Add Child to first First"
Command="{Binding AddFirstFirstChildCommand}" />
<Button Content="Toggle Expand"
Command="{Binding ToggleExpandCollapseCommand}" />
<Button Content="Add Child to second First"
Command="{Binding AddSecondFirstChildCommand}" />
</StackPanel>
<Button Content="Bad Codebehind Button"
Click="Button_Click"/>
</StackPanel>
</Grid>
</Window>
this returns array of Level1 from first table in DataSet (usually there's only one table)
public void Level1[] GetLevels()
{
DataSet ds = ....
return ds.Tables[0].Rows
.Select(row => new Level1((string)row["name"]))
.ToArray();
}
if you had more than one table in the dataset, you can use this method to loop trough all tables:
public void Level1[] GetLevels()
{
DataSet ds = ....
return ds.Tables
.SelectMany(t => t.Rows)
.Select(row => new Level1((string)row["name"]))
.ToArray();
}
The second code sample does exactly the same as your code in the question.
Understanding linq is extremely useful.

WPF combo box not binding on change

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.

How can I tie two combo boxes together in wpf

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.

Categories

Resources