Update binding of TreeViewItem using code? (XAML / WPF) - c#

I have a TreeView whose ItemsSource is set to a Model I have. 3 levels deep is an object whose state can change and, rather than write a View and ViewModel for each stage (very lengthy business) I wanted to just update this using code.
So basically I have an event that is fired once my model updates, I catch it then find the TreeViewItem associated with this bit of data. My issue now is I have NO idea on how to update the binding on it to reflect the new value!
Can anyone help?
I know this isn't best practice but I really don't want to have to waste time writing a massive amount of code to just update one thing.
Thanks
Chris

Are you sure it wouldn't be easier to implement INotifyPropertyChanged on the relevant class?

This example works, though it's only two (not 3) levels deep. It shows a simple 2-level hierarchical treeview with parent items A, B, and C, with numbered children (A.1, B.1, etc). When the Rename B.1 button is clicked, it renames B.1 to "Sylvia".
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace UpdateVanillaBindingValue
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
private DataClass _data;
public Window1()
{
InitializeComponent();
var data = CreateData();
DataContext = _data = data;
}
private DataClass CreateData()
{
return new DataClass
{
Parents=new List<Parent>
{
new Parent{Name="A",Children=new List<Child>{new Child{Name="A.0"},new Child{Name="A.1"}}},
new Parent{Name="B",Children=new List<Child>{new Child{Name="B.0"},new Child{Name="B.1"},new Child{Name="B.2"}}},
new Parent{Name="C",Children=new List<Child>{new Child{Name="C.0"},new Child{Name="C.1"}}}
}
};
}
private void Rename_Click(object sender, RoutedEventArgs e)
{
var parentB = _data.Parents[1];
var parentBItem = TheTree.ItemContainerGenerator.ContainerFromItem(parentB) as TreeViewItem;
parentB.Children[1].Name = "Sylvia";
var parentBItemsSource = parentBItem.ItemsSource;
parentBItem.ItemsSource = null;
parentBItem.ItemsSource = parentBItemsSource;
}
}
public class DataClass
{
public List<Parent> Parents { get; set; }
}
public class Parent
{
public string Name { get; set; }
public List<Child> Children { get; set; }
}
public class Child
{
public string Name { get; set; }
}
}
<Window x:Class="UpdateVanillaBindingValue.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Grid.Resources>
<DataTemplate x:Key="ChildTemplate">
<TextBlock Margin="50,0,0,0" Text="{Binding Name}" />
</DataTemplate>
<HierarchicalDataTemplate x:Key="ParentTemplate" ItemsSource="{Binding Children}" ItemTemplate="{StaticResource ChildTemplate}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</Grid.Resources>
<TreeView x:Name="TheTree" ItemsSource="{Binding Parents}" ItemTemplate="{StaticResource ParentTemplate}" />
<Button VerticalAlignment="Bottom" HorizontalAlignment="Center" Content="Rename B.1" Click="Rename_Click" />
</Grid>
</Window>
This is a hack, but it re-evaluates the DataTemplate every time it's ItemsSource property changes.
Ideally, you would implement INotifyPropertyChanged on your model object class that this TreeViewItem is bound to, and have it fire the PropertyChanged event when that value changes. In fact, you should be careful that you aren't incurring a memory leak because it doesn't: Finding memory-leaks in WPF Applications.

Sounds like you might need to take a look at the UpdateSource and UpdateTarget methods.
MSDN reference for UpdateSource
Although I'm not totally sure this will work when you've actually bound the TreeView's ItemSource to a hierarchical data structure, but it's where I would start investigating.

Related

WPF binding source mismatch for a MahApps.Metro.Controls.NumericUpDown inside a TabControl

I often use MahApps.Metro.Controls.NumericUpDown to manipulate numerical values from the user interface. Recently I have observed a binding behaviour so strange and surreal that completely undermined my faith in the WPF binding infrastructure.
The phenomenon occurred with a collection of view models bound to the ItemsSource of a TabControl. Within each of these view models, there is a property of type int that I bind to the Value property of a MahApps.Metro.Controls.NumericUpDown. I noticed that when I manipulate a NumericUpDown through its TextBox and leave the focus there just before I select a new tab in the TabControl, the value I have just set gets written into the NumericUpDown inside the new tab being selected. That is clearly undesirable as that tab contains a value I did not intend to overwrite just by selecting the containing tab.
This only occurs if I use the TextBox part to set the value, and not when I use the + and - buttons. Furthermore, taking the focus from the NumericUpDown prevents the bug from occurring. Hooking into the LostFocus event of the NumericUpDown, I could observe that the binding source of the respective binding had already changed to that within the view model associated with the next tab (instead of that associated with the NumericUpDown actually firing the LostFocus event), and that explains the surreal value bleed. (Additionally, it gets fired twice, as you will be able to see in the MWE.)
I could find a workaround by registering for the PreviewKeyDown event of the tab label and setting the focus to another element programmatically before the tab switch actually occurs. Still, as I said, this undermined my confidence in the WPF binding architecture. Is it a general WPF binding bug or is it specific to MahApps.Metro.Controls.NumericUpDown? What can I do to ensure such bugs do not occur in my code (apart from the amateur workaround I could come up with)?
The XAML part of the MWE:
<Window x:Class="NumericUpDownMWE.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:mahApps="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:local="clr-namespace:NumericUpDownMWE"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TabControl
ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<TextBlock Text="Value" />
<mahApps:NumericUpDown
LostFocus="HandleLostFocus"
Value="{Binding Value}" />
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
The respective code-behind:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace NumericUpDownMWE
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ExampleViewModel();
}
private void HandleLostFocus(object sender, RoutedEventArgs e)
{
FrameworkElement? frameworkElement = sender as FrameworkElement;
BindingExpression? bindingExpression = frameworkElement?.GetBindingExpression(MahApps.Metro.Controls.NumericUpDown.ValueProperty);
string? sourceName = (bindingExpression?.ResolvedSource as ExampleItem)?.Name;
}
}
public class ExampleItem : INotifyPropertyChanged
{
private int value;
public event PropertyChangedEventHandler? PropertyChanged;
public string Name { get; }
public int Value {
get { return value; }
set {
if (value != this.value) {
this.value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
}
public ExampleItem(string name, int value)
{
this.Name = name;
this.value = value;
}
}
public class ExampleViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<ExampleItem> Items { get; }
public ExampleViewModel()
{
this.Items = new ObservableCollection<ExampleItem>() {
new ExampleItem("Item A", 1),
new ExampleItem("Item B", 2),
new ExampleItem("Item C", 3)
};
}
}
}

Entity Framework with Prism MVVM: How to get an auto incremented id to a CollectionViewSource?

I'm new to EF and trying to get the EF Core Getting Started with WPF tutorial running with Prism MVVM.
I'm currently stuck with an ugly solution to reflect the auto-incremented id (sqlite) for an inserted item back to the DataGrid after pressing the Save button. Update: I later found out that all sorting and filtering is lost when done this way.
In the non-mvvm tutorial this is done by calling productsDataGrid.Items.Refresh(). That works nicely:
private void Button_Click(object sender, RoutedEventArgs e)
{
_context.SaveChanges();
productsDataGrid.Items.Refresh();
}
The only solution (Update: See below for a better solution.) that currently works for me is to set the ObservableCollection to null and then re-assign it to the database context after calling context.SaveChanges() in my Save() function.
This is the working code (which discards filtering and sorting):
MainWindow.xaml
<Window x:Class="EfTestPrism.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:EfTestPrism"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:MainWindowViewModel, IsDesignTimeCreatable=True}"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<CollectionViewSource x:Key="CategoryViewSource"
Source="{Binding CategoriesCollection}"/>
</Window.Resources>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closing">
<i:InvokeCommandAction Command="{Binding WindowCloseCommand, Mode=OneTime}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DataGrid Grid.Row="0"
AutoGenerateColumns="False"
RowDetailsVisibilityMode="VisibleWhenSelected"
ItemsSource="{Binding
Source={StaticResource CategoryViewSource}}">
<DataGrid.Columns>
<DataGridTextColumn Header="Category Id"
Width="SizeToHeader"
IsReadOnly="True"
Binding="{Binding CategoryId}"/>
<DataGridTextColumn Header="Name"
Width="*"
Binding="{Binding Name}"/>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1"
Content="Save"
Command="{Binding SaveCommand}"/>
</Grid>
</Window>
MainWindow.xaml.cs:
namespace EfTestPrism;
public partial class MainWindow
{
public MainWindow() {
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
MainWindowViewModel.cs
using System.Collections.ObjectModel;
using System.Windows.Input;
using Microsoft.EntityFrameworkCore;
using Prism.Commands;
using Prism.Mvvm;
namespace EfTestPrism;
public class MainWindowViewModel : BindableBase
{
public MainWindowViewModel() {
context.Database.EnsureCreated();
context.Categories!.Load();
CategoriesCollection = context.Categories!.Local.ToObservableCollection();
}
private readonly ProductContext context = new ();
private ObservableCollection<Category> ? categoriesCollection;
public ObservableCollection<Category> ? CategoriesCollection {
get => categoriesCollection!;
set => SetProperty(ref categoriesCollection, value);
}
public ICommand SaveCommand => new DelegateCommand(Save);
private void Save() {
context.SaveChanges();
/* I don't like the following but it works.
I tried different things here, see below. */
CategoriesCollection = null;
CategoriesCollection = context.Categories!.Local.ToObservableCollection();
}
public ICommand WindowCloseCommand => new DelegateCommand(WindowClose);
private void WindowClose() {
context.Dispose();
}
}
ProductContext.cs
using Microsoft.EntityFrameworkCore;
namespace EfTestPrism;
public class ProductContext : DbContext
{
public DbSet<Category> ? Categories { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) {
options.UseSqlite("Data Source=products.db");
options.UseLazyLoadingProxies();
}
}
Category.cs
namespace EfTestPrism;
public class Category // I tried making this a BindableBase and...
{
public int CategoryId { get; set; } // ...using SetProperty without success
public string Name { get; set; }
}
Things I've tried without success:
ViewModel::Save() function:
RaisePropertyChanged(nameof(CategoriesCollection)
Refreshing each collection item and/or id property:
.
foreach (var item in CategoriesCollection) {
RaisePropertyChanged(nameof(item.CategoryId));
RaisePropertyChanged(nameof(item));
}
Setting the id to zero and back to the original value. Strange things happen here like all ids being zero in the data grid except for the newly added items:
.
foreach (var item in oc) {
var temp = item.CategoryId;
item.CategoryId = 0;
item.CategoryId = temp;
}
MainWindow.xaml:
Trying all UpdateSourceTriggers for the CategoryID binding.
I can see that the collection changes. When I remove the IsReadonly="True" on the DataGrids CategoryId column, the value gets updated as soon as I double click it after saving (I don't know if the UI is just updated or it actually syncs with the database).
What would be a proper mvvm way to update the DataGrid similarly to the categoryDataGrid.Items.Refresh(); call after _context.SaveChanges() in the Button_Click function of the tutorial?
Update: Refresh callback from ViewModel to View
The following works and keeps sorting and filtering. I don't mind too much about the code behind because it's strictly view related and I think that's acceptable.
Pro: No manual impementation of removing and adding back the items to the collection i.e. least code that works (if there's not a better solution).
Con: The view model has to call a delegate. So it actually has to anticipate that the view it's used in might want to provide a callback.
Changes to the above code:
MainWindow.xaml: Add an x:Name to the DataGrid to make it accessable from the code behind:
[...]
<DataGrid Grid.Row="0"
x:Name="CategoriesDataGrid"
AutoGenerateColumns="False"
[...]
Add a delegate to MainWindowViewModel.cs and call it in Save():
[...]
public delegate void Callback();
public class MainWindowViewModel : BindableBase
{
public MainWindowViewModel(Callback ? refreshView = null) {
RefreshView = refreshView;
[...]
private readonly Callback ? RefreshView;
[...]
private void Save() {
context.SaveChanges();
RefreshView?.Invoke();
}
[...]
Implement and supply a RefreshView method in MainWindow.xaml.cs:
namespace EfTestPrism;
public partial class MainWindow
{
public MainWindow() {
InitializeComponent();
DataContext = new MainWindowViewModel(RefreshView);
}
private void RefreshView() {
CategoriesDataGrid.Items.Refresh();
}
}
Unless EF sets the property of the existing item in memory for you, you need to load the new ids from the database and update the CategoryId property of your Category items yourself.
The easiest way to do this is to basically do the same thing that you do in the constructor after you have called SaveChanges(), e.g. set the collection property to a new collection with fresh items from the database:
private void Save() {
context.SaveChanges();
CategoriesCollection = new ObservableCollection<Category>(context.Categories);
}
Then you should get a new collection with new updated Category items.
Alternatively, you could Clear() the items in the existing collection and then add the new items from the database to it:
private void Save()
{
context.SaveChanges();
CategoriesCollection.Clear();
foreach (var category in context.Categories.ToArray())
CategoriesCollection.Add(category);
}
What you want is to map your entities (Category) to view models (derived from BindableBase) and manually update the view models when the entities change (that includes updating the view model collection when the entities collection changes).
This way the data grid keeps its sorting, filtering, scroll postion etc. when you update items.
RaisePropertyChanged(nameof(CategoryId)) works only when called from inside the category - it raises the PropertyChanged event on its instance - that is, MainWindowViewModel and explains why the data grid doesn't care and does not update its cells.

Set itemtemplate for a listbox inside usercontrol

I have a Listbox inside a UserControl to manage different entities with different properties. The UserControl is the same for all entities.
I use MVVM and i bind a generic ViewModel as DataContext for the UserControl.
I wish to set the ItemTemplate for the Listbox in the container xaml for the UserControl in order to display the entity properties.
For the entity "Emlployee" I need to display FullName, for the entity Certifying I need to display CertifyingAuthor and CertifyingDate and so on.
What I need is something similar to that
<StackPanel Grid.Row="0" Orientation="Vertical">
<uc:SearchableFromListTextBox ItemTemplateForTheInsideListBox="{StaticResource Something}" ></uc:SearchableFromListTextBox>
Should I add a dependencyProperty ItemTemplateForTheInsideListBoxProperty to the UserControl? And how i could pass it as itemtemplate of the Listbox?
Hope the question is well explained considering my italian native language.
thanks
EDIT : I give up. This is a control for keyboard data entry and something similar to autocomplete.
Since i am forced to agree to a compromise with MVVM :( i will choose some dirty way to resolve.
Thanks to all
A DataTemplateSelector can do what I think you want. You can define different templates from your user control's XAML, then have the selector choose among them.
Assume these model classes:
public class Employee
{
public Employee(string fullName)
{
FullName = fullName;
}
public string FullName { get; }
}
public class Certifying
{
public Certifying(string certifyingAuthor, DateTime certifyingDate)
{
CertifyingAuthor = certifyingAuthor;
CertifyingDate = certifyingDate;
}
public string CertifyingAuthor { get; }
public DateTime CertifyingDate { get; }
}
Your user control's XAML has a ListBox, which in turn uses a template selector (that we'll get to in a moment) -- and also defines different templates for the two different model types:
<UserControl
x:Class="WPF.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPF"
>
<Grid>
<ListBox x:Name="listBox">
<ListBox.ItemTemplateSelector>
<local:MyTemplateSelector>
<local:MyTemplateSelector.EmployeeTemplate>
<DataTemplate DataType="local:Employee">
<TextBlock
Foreground="Red"
Text="{Binding FullName}"
/>
</DataTemplate>
</local:MyTemplateSelector.EmployeeTemplate>
<local:MyTemplateSelector.CertifyingTemplate>
<DataTemplate DataType="local:Certifying">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Foreground="Blue"
Text="{Binding CertifyingAuthor}"
/>
<TextBlock
Grid.Column="1"
Foreground="Green"
Text="{Binding CertifyingDate}"
/>
</Grid>
</DataTemplate>
</local:MyTemplateSelector.CertifyingTemplate>
</local:MyTemplateSelector>
</ListBox.ItemTemplateSelector>
</ListBox>
</Grid>
</UserControl>
The user control's code-behind, where I just assign a list of model objects to the list box (for simplicity):
public partial class UserControl1
{
public UserControl1()
{
InitializeComponent();
listBox.ItemsSource = new List<object>
{
new Employee("Donald Duck"),
new Certifying("Mickey Mouse", DateTime.Now),
new Employee("Napoleon Bonaparte"),
new Certifying("Homer Simpson", DateTime.Now - TimeSpan.FromDays(2)),
};
}
}
And finally, the template selector. Its two properties were set from the user control's XAML; the logic of SelectTemplate decides which one to apply:
public class MyTemplateSelector : DataTemplateSelector
{
/// <summary>
/// This property is set from XAML.
/// </summary>
public DataTemplate EmployeeTemplate { get; set; }
/// <summary>
/// This property is set from XAML.
/// </summary>
public DataTemplate CertifyingTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is Employee)
{
return EmployeeTemplate;
}
if (item is Certifying)
{
return CertifyingTemplate;
}
return base.SelectTemplate(item, container);
}
}
And, for completeness, usage of the user control:
<Grid>
<wpf:UserControl1 />
</Grid>
The end result, where Napoleon is currently selected, and Homer suffers a mouse-over:

Binding tab controls with mahapps and prism - WPF

I am building a WPF application with mahapps, prism[modularity]. I have below HomeWindow.xaml code.
<Controls:MetroWindow x:Class="Project.Views.HomeWindow"
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:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:local="clr-namespace:Project.Views"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
<!--The above code is for automatically binding of viewmodel into view-->
Height="700" Width="1200" Background="White">
<Grid>
<TabControl ItemsSource="{Binding TabCollection}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock Text="{Binding Name}"/>
</TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Label Content="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Controls:MetroWindow>
I have below structure in my HomeViewModel.cs under ViewModels directory.
public class HomeViewModel : BindableBase
{
private ObservableCollection<Item> _tabCollection;
public ObservableCollection<Item> TabCollection { get { return _tabCollection; } set { SetProperty(ref _tabCollection, value); } }
//Prism way of getting and setting data
}
public class Item
{
private string Name;
private string Content;
public Item(string name, string content)
{
Name = name;
Content = content;
}
}
below is how I add data into TabCollection property through HomeWindow.xaml.cs.
private HomeViewModel _model=new HomeViewModel();
public HomeWindow(EmployeeViewModel model)
{
InitializeComponent();
_model.UserViewModel = model;
LoadHomeData(_model.UserViewModel.EmpRole);
DataContext = this;
}
private void LoadHomeData(string Role)
{
if (string.Equals(Role, "Admin"))
{
_model.TabCollection= new ObservableCollection<Item>()
{
new Item("Test1", "1"),
new Item("Test2", "2"),
new Item("Test3", "3")
};
}
}
Now matter what, the tabs will not get displayed. Its a blank empty window. I have followed the example in the issue here and have went through few similar posts having same kind of approach. But none of them helped. Is this because of prism way of databinding or is there anything else am missing here? Hope to find some help on this..
Your problem is not connected to MahApps or Prism but to how WPF works in general. In your case Name and Content are private fields and should be public properties
public string Name { get; set; }
public string Content { get; set; }
private or field is not a valid binding source. You can find more as to what is a valid binding source under Binding Sources Overview but in your case, as far as CLR object goes:
You can bind to public properties, sub-properties, as well as indexers, of any common language runtime (CLR) object. The binding engine uses CLR reflection to get the values of the properties. Alternatively, objects that implement ICustomTypeDescriptor or have a registered TypeDescriptionProvider also work with the binding engine.
Another problem is that DataContext is set wrong. At the moment is set to HomeWindow and I think it should be set to instance of HomeViewModel which holds TabCollection property
DataContext = _model;

Correct use of WPF view model

I'm teaching myself WPF. My window has two combo boxes: one for Categories and one for Subcategories. When the category selection changes, I want the list of subcategories to update to just those that are in the selected category.
I've created a simple view class for both of the combo boxes. My SubcategoryView class' constructor takes a reference to my CategoryView class and attaches an event handler for when the category selection changes.
public class SubcategoryView : INotifyPropertyChanged
{
protected CategoryView CategoryView;
public SubcategoryView(CategoryView categoryView)
{
CategoryView = categoryView;
CategoryView.PropertyChanged += CategoryView_PropertyChanged;
}
private void CategoryView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedItem")
{
_itemsSource = null;
}
}
private ObservableCollection<TextValuePair> _itemsSource;
public ObservableCollection<TextValuePair> ItemsSource
{
get
{
if (_itemsSource == null)
{
// Populate _itemsSource
}
return _itemsSource;
}
}
}
I assign my DataContexts like this.
cboCategory.DataContext = new CategoryView();
cboSubcategory.DataContext = new SubcategoryView(cboCategory.DataContext as CategoryView);
The problem is that selecting a new item in my category combo box does not cause the subcategories to repopulate (even though I confirmed my PropertyChanged handler is being called).
What is the correct way to cause the list to repopulate?
Also, I welcome any other comments about this approach. Instead of passing my CategoryView to the constructor, is it better to indicate this declaratively somehow in the XAML?
Here's how we do it in production code.
Each category knows what its subcategories are. If they're coming from a database or a disk file, the database/webservice method/file reader/whatever would return classes just like that, and you'd create the viewmodels to match. The viewmodel understands the structure of the information but knows and cares nothing about the actual content; somebody else is in charge of that.
Note that this is all very declarative: The only loop is the one that fakes up the demo objects. No event handlers, nothing in codebehind except creating the viewmodel and telling it to populate itself with fake data. In real life you do often end up writing event handlers for special cases (drag and drop, for example). There's nothing non-MVVMish about putting view-specific logic in the codebehind; that's what it's there for. But this case is much too trivial for that to be necessary. We have a number of .xaml.cs files that have sat in TFS for years on end exactly as the wizard created them.
The viewmodel properties are a lot of boilerplate. I have snippets (steal them here) to generate those, with the #regions and everything. Other people copy and paste.
Usually you'd put each viewmodel class in a separate file, but this is example code.
It's written for C#6. If you're on an earlier version we can change it to suit, let me know.
Finally, there are cases where it makes more sense to think in terms of having one combobox (or whatever) filtering another large collection of items, rather than navigating a tree. It can make very little sense to do that in this hierarchical format, particularly if the "category":"subcategory" relationship isn't one-to-many.
In that case, we'd have a collection of "categories" and a collection of all "subcategories", both as properties of the main viewmodel. We would then use the "category" selection to filter the "subcategory" collection, usually via a CollectionViewSource. But you could also give the viewmodel a private full list of all "subcategories" paired with a public ReadOnlyObservableCollection called something like FilteredSubCategories, which you'd bind to the second combobox. When the "category" selection changes, you repopulate FilteredSubCategories based on SelectedCategory.
The bottom line is to write viewmodels which reflect the semantics of your data, and then write views that let the user see what he needs to see and do what he needs to do. Viewmodels shouldn't be aware that views exist; they just expose information and commands. It's often handy to be able to write multiple views that display the same viewmodel in different ways or at different levels of detail, so think of the viewmodel as just neutrally exposing any information about itself that anybody might want to use. Usual factoring rules apply: Couple as loosely as possible (but no more loosely), etc.
ComboDemoViewModels.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace ComboDemo.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] String propName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
#endregion INotifyPropertyChanged
}
public class ComboDemoViewModel : ViewModelBase
{
// In practice this would probably have a public (or maybe protected) setter
// that raised PropertyChanged just like the other properties below.
public ObservableCollection<CategoryViewModel> Categories { get; }
= new ObservableCollection<CategoryViewModel>();
#region SelectedCategory Property
private CategoryViewModel _selectedCategory = default(CategoryViewModel);
public CategoryViewModel SelectedCategory
{
get { return _selectedCategory; }
set
{
if (value != _selectedCategory)
{
_selectedCategory = value;
OnPropertyChanged();
}
}
}
#endregion SelectedCategory Property
public void Populate()
{
#region Fake Data
foreach (var x in Enumerable.Range(0, 5))
{
var ctg = new ViewModels.CategoryViewModel($"Category {x}");
Categories.Add(ctg);
foreach (var y in Enumerable.Range(0, 5))
{
ctg.SubCategories.Add(new ViewModels.SubCategoryViewModel($"Sub-Category {x}/{y}"));
}
}
#endregion Fake Data
}
}
public class CategoryViewModel : ViewModelBase
{
public CategoryViewModel(String name)
{
Name = name;
}
public ObservableCollection<SubCategoryViewModel> SubCategories { get; }
= new ObservableCollection<SubCategoryViewModel>();
#region Name Property
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged();
}
}
}
#endregion Name Property
// You could put this on the main viewmodel instead if you wanted to, but this way,
// when the user returns to a category, his last selection is still there.
#region SelectedSubCategory Property
private SubCategoryViewModel _selectedSubCategory = default(SubCategoryViewModel);
public SubCategoryViewModel SelectedSubCategory
{
get { return _selectedSubCategory; }
set
{
if (value != _selectedSubCategory)
{
_selectedSubCategory = value;
OnPropertyChanged();
}
}
}
#endregion SelectedSubCategory Property
}
public class SubCategoryViewModel : ViewModelBase
{
public SubCategoryViewModel(String name)
{
Name = name;
}
#region Name Property
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged();
}
}
}
#endregion Name Property
}
}
MainWindow.xaml
<Window
x:Class="ComboDemo.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:ComboDemo"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Orientation="Vertical" Margin="4">
<StackPanel Orientation="Horizontal">
<Label>Categories</Label>
<ComboBox
x:Name="CategorySelector"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="20,4,4,4">
<Label>Sub-Categories</Label>
<ComboBox
ItemsSource="{Binding SelectedCategory.SubCategories}"
SelectedItem="{Binding SelectedCategory.SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace ComboDemo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new ViewModels.ComboDemoViewModel();
vm.Populate();
DataContext = vm;
}
}
}
Extra Credit
Here's a different version of MainWindow.xaml, which demonstrates how you can show the same viewmodel in two different ways. Notice that when you select a category in one list, that updates SelectedCategory which is then reflected in the other list, and the same is true of SelectedCategory.SelectedSubCategory.
<Window
x:Class="ComboDemo.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:ComboDemo"
xmlns:vm="clr-namespace:ComboDemo.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<DataTemplate x:Key="DataTemplateExample" DataType="{x:Type vm:ComboDemoViewModel}">
<ListBox
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:CategoryViewModel}">
<StackPanel Orientation="Horizontal" Margin="2">
<Label Width="120" Content="{Binding Name}" />
<ComboBox
ItemsSource="{Binding SubCategories}"
SelectedItem="{Binding SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="120"
/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</Window.Resources>
<Grid>
<StackPanel Orientation="Vertical" Margin="4">
<StackPanel Orientation="Horizontal">
<Label>Categories</Label>
<ComboBox
x:Name="CategorySelector"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="20,4,4,4">
<Label>
<TextBlock Text="{Binding SelectedCategory.Name, StringFormat='Sub-Categories in {0}:', FallbackValue='Sub-Categories:'}"/>
</Label>
<ComboBox
ItemsSource="{Binding SelectedCategory.SubCategories}"
SelectedItem="{Binding SelectedCategory.SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<GroupBox Header="Another View of the Same Thing" Margin="4">
<!--
Plain {Binding} just passes along the DataContext, so the
Content of this ContentControl will be the main viewmodel.
-->
<ContentControl
ContentTemplate="{StaticResource DataTemplateExample}"
Content="{Binding}"
/>
</GroupBox>
</StackPanel>
</Grid>
</Window>
Using single view-model in that case is really simpler, as mentioned in comments. For example, I'll use just strings for combo box items.
To demonstrate correct using of view model, we'll track changes of category through binding rather than UI event. So, besides ObservableCollections you'll need SelectedCategory property.
View-model:
public class CommonViewModel : BindableBase
{
private string selectedCategory;
public string SelectedCategory
{
get { return this.selectedCategory; }
set
{
if (this.SetProperty(ref this.selectedCategory, value))
{
if (value.Equals("Category1"))
{
this.SubCategories.Clear();
this.SubCategories.Add("Category1 Sub1");
this.SubCategories.Add("Category1 Sub2");
}
if (value.Equals("Category2"))
{
this.SubCategories.Clear();
this.SubCategories.Add("Category2 Sub1");
this.SubCategories.Add("Category2 Sub2");
}
}
}
}
public ObservableCollection<string> Categories { get; set; } = new ObservableCollection<string> { "Category1", "Category2" };
public ObservableCollection<string> SubCategories { get; set; } = new ObservableCollection<string>();
}
Where SetProperty is implementation of INotifyPropertyChanged.
When you select category, the setter of SelectedCategory property triggers and you can fill subcatagory items depending on selected category value. Do not replace collection object itself! You should clear existing items and then add new ones.
In xaml, besides ItemsSource for both combo boxes, you'll need bind SelectedItem for category combo box.
XAML:
<StackPanel x:Name="Wrapper">
<ComboBox ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, Mode=OneWayToSource}" />
<ComboBox ItemsSource="{Binding SubCategories}" />
</StackPanel>
Then just assign view-model to wrapper's data context:
Wrapper.DataContext = new CommonViewModel();
And code for BindableBase:
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

Categories

Resources