Bind property between sibling user controls - c#

I have the below problem: I have two different user controls inside a parent user control. These are trainList, which holds a list of train objects and trainView, which is an user control that shows details of the selected train in the list.
My wish is to share a variable of trainList with trainView.
What I have now is:
Parent user control:
<UserControl>
<UserControl>
<customControls:trainList x:Name="trainList"></customControls:trainList>
</UserControl>
<UserControl>
<customControls:trainView x:Name="trainView"></customControls:trainView>
</UserControl>
<TextBlock DataContext="{Binding ElementName=trainList, Path=SelectedTrain}" Text="{ Binding SelectedTrain.Id }">Test text</TextBlock>
</UserControl>
TrainList class:
public partial class TrainList : UserControl
{
public TrainList()
{
this.InitializeComponent();
this.DataContext = this;
}
public Train SelectedTrain { get; set; }
public void SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Debug.Print(this.SelectedTrain.Id);
}
}
Note: The Train class implements INotifyPropertyChanged.
If I got this to work, I'd apply the binding to the trainView user control (not sure if this would work) instead to the text block.
<UserControl>
<customControls:trainView x:Name="trainView" DataContext="{Binding ElementName=trainList, Path=SelectedTrain}"></customControls:trainView>
</UserControl>
And then, I would access that variable someway from the code-behind of trainView.
(And after this, I would like to share a different variable from trainView with its parent user control, but maybe that's another question).
My current question is: could this be done this way or would I need to follow another strategy?

Take this simple view model, with a base class that implements the INotifyPropertyChanged interface, and a Train, TrainViewModel and MainViewModel class.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(
ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (!Equals(storage, value))
{
storage = value;
OnPropertyChanged(propertyName);
}
}
}
public class Train : ViewModelBase
{
private string name;
public string Name
{
get { return name; }
set { SetValue(ref name, value); }
}
private string details;
public string Details
{
get { return details; }
set { SetValue(ref details, value); }
}
// more properties
}
public class TrainViewModel : ViewModelBase
{
public ObservableCollection<Train> Trains { get; }
= new ObservableCollection<Train>();
private Train selectedTrain;
public Train SelectedTrain
{
get { return selectedTrain; }
set { SetValue(ref selectedTrain, value); }
}
}
public class MainViewModel
{
public TrainViewModel TrainViewModel { get; } = new TrainViewModel();
}
which may be initialized in the MainWindow's constructor like this:
public MainWindow()
{
InitializeComponent();
var vm = new MainViewModel();
DataContext = vm;
vm.TrainViewModel.Trains.Add(new Train
{
Name = "Train 1",
Details = "Details of Train 1"
});
vm.TrainViewModel.Trains.Add(new Train
{
Name = "Train 2",
Details = "Details of Train 2"
});
}
The TrainDetails controls would look like this, of course with more elements for more properties of the Train class:
<UserControl ...>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Details}"/>
</StackPanel>
</UserControl>
and the parent UserControl like this, where I directly use a ListBox instead of a TrainList control:
<UserControl ...>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox ItemsSource="{Binding Trains}"
SelectedItem="{Binding SelectedTrain}"
DisplayMemberPath="Name"/>
<local:TrainDetailsControl Grid.Column="1" DataContext="{Binding SelectedTrain}"/>
</Grid>
</UserControl>
It would be instantiated in the MainWindow like this:
<Grid>
<local:TrainControl DataContext="{Binding TrainViewModel}"/>
</Grid>
Note that in this simple example the elements in the UserControls' XAML bind directly to a view model instance that is passed via their DataContext. This means that the UserControl know the view model (or at least their properties). A more general approach is to declare dependency properties in the UserControl class, that are bound to view model properties. The UserControl would then be independent of any particular view model.

Related

TextBox inside a ListView bound to an object, two way binding dosen't work

Edit:
Ok after finally playing around numerous times without no luck, I have created a very small Wpf application. You can directly copy this code. Notice when you change values in the TextBox and press the Test button, the values never get updated. I don't understand why the two way binding dosen't work. Please help.
Here is the xaml:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView Grid.Row="0"
ItemsSource="{Binding Path=Demo.CurrentParameterValue,Mode=TwoWay}"
HorizontalAlignment="Center" VerticalAlignment="Center">
<ListView.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=.,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Width="100"></TextBox>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Grid.Row="1" Click="Button_Click">TEST</Button>
</Grid>
Here is the xaml.cs:
namespace WpfApp9
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private VmServiceMethodsViewDataGridModel _demo;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public VmServiceMethodsViewDataGridModel Demo
{
get => _demo;
set
{
_demo = value;
OnPropertyChanged("Demo");
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
Demo = new VmServiceMethodsViewDataGridModel();
Demo.CurrentParameterValue.Add(1);
Demo.CurrentParameterValue.Add(2);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var collection = Demo.CurrentParameterValue;
MessageBox.Show(string.Format("Values are {0}, {1}", collection[0], collection[1]));
}
}
public class VmServiceMethodsViewDataGridModel : INotifyPropertyChanged
{
private List<object> _currentParameterValue;
public List<object> CurrentParameterValue
{
get => _currentParameterValue;
set
{
_currentParameterValue = value;
OnPropertyChanged("CurrentParameterValue");
}
}
public VmServiceMethodsViewDataGridModel()
{
CurrentParameterValue = new List<object>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
The problem with your binding is that you are trying to bind to an object. This is perfectly fine in a OneWay/OneTime scenario. But not when using binding TwoWay. You can change the value of a property e.g. in your view model, but you can't change the object instance itself. In your specific case, the binding would have to send the new long input to the view model's value collection and replace the old value. Of course this will never happen as Binding is not designed to work this way.
The technical reason is that changing the instance would mean to change the Binding.Source. Once the binding is active (controlled by a BindingExpression) it becomes immutable. Changing the source is not allowed. That's also the reason why {Binding Source={DynamicResource ...}} won't work. The BindingSource can only be static (or StaticResource - not changing resource).
You usually bind to properties. In a TwoWay binding scenario Binding can simply update the property's value. So the solution to your problem is to wrap the long values into a class and bind the TextBox to a property of this class to retrieve/modify the actual value.
In this context your code looks too complicated.
Your object structure is too complex or unnatural.
You don't need to apply the DataTemplate to a ContentControl (in XAML).
And of course as this is a UWP application, use x:Bind where possible as it will improve performance. The converter is redundant as Binding and x:Bind allow a nested PropertyPath e.g.
<ListView ItemsSource="{Binding CurrentParameterValue.ListParameterValues}">
ItemsControl.ItemsSource doesn't need a TwoWay binding. The ItemsControl will never update/replace the source collection. If you don plan to replace the source collection in the view model (e.g., AtlasMethodParameterList = new ObservableCollection<>()), then you can even set the binding mode to OneTime (which would be the default for x:Bind).
I recommend to use OneTime and if you need to replace the collection, rather call Clear() on the collection and add the new items. This will improve the performance.
Never use async void in a method signature except for event handlers.
Always use async Task, when the return type is void or when returning a value async Task<TResult>. Otherwise you will experience unexpected side effects, especially when encountering exceptions:
// An async void method must return Task
private async Task GetParameterList(string obj)
Also async methods should always be awaited. This means the method calling and awaiting an async method must itself return Task or Task<T> to be awaitable. A method returning type void cannot be awaited.
All DependencyProperty of every control, have their Binding.UpdateSourceTrigger set to UpdateSourceTrigger.PropertyChanged by default.
Exceptions are properties that are likely to raise too much consecutive property changes like a TextBox would do on each input/key press. TextBox.Text has the default set to UpdateSourceTrigger.LostFocus.
You should remove all redundant UpdateSourceTrigger.PropertyChanged from the bindings to improve readability.
Consider to use out instead of ref if you don't intend to read the variable. If you only set the value prefer to use out to hint your intent to any reader. Use in if don't intent to modify the reference (read-only reference).
Your Set method should look something like this:
protected virtual void Set<TValue>(out TValue valueTarget, TValue value, [CallerMemberName] string propertyName = null)
{
if (value != valueTarget)
{
valueTarget = value;
OnPropertyChanged(propertyName);
}
}
I refactored your complete code trying to improve it:
Parameter.cs
// The type that wraps the actual parameter value.
// Consider to use dedicated types e.g., LongParameter instead, to allow a strongly typed Value property instead of a basic property of type object.
// This prevents implicit boxing/unboxing in order to convert from object/reference type to primitive/value type and vice versa. This will improve performance.
// (Only needed because we are dealing with primitive/value types like long, double, etc)
// You would then have to define a DataTemplate for each type. Don't forget to set x:DataType on each DataTemplate.
public class Parameter : BindableBase
{
protected Parameter(object value)
{
this.Value = value;
}
private object value;
public object Value
{
get => this.value;
set => Set(out this.value, value);
}
}
VmServiceModel.cs
public class VmServiceModel : BindableBase
{
public VmServiceModel()
{
this.Parameters = new List<Parameter>();
}
private List<Parameter> _parameters;
public List<Parameter> Parameters
{
get => this._parameters;
set => Set(out this._parameters, value);
}
}
ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
this.AtlasMethodParameterList = new ObservableCollection<VmServiceModel>();
}
private ObservableCollection<VmServiceModel> _atlasMethodParameterList;
public ObservableCollection<VmServiceModel> AtlasMethodParameterList
{
get => _atlasMethodParameterList;
set => Set(out _atlasMethodParameterList, value);
}
private async Task GetParameterList(string obj)
{
foreach (var item in this.ParametersCollection)
{
var vmServiceModel = new VmServiceModel();
vmServiceModel.Parameters
.AddRange(item.Value.Cast<long>().Select(innerItem => new Parameter(innerItem)));
this.AtlasMethodParameterList.Add(vmServiceModel);
}
}
}
MainPage.xaml.cs
public sealed partial class MainPage : Page
{
public ViewModel ViewModel { get; set; }
public MainPage()
{
this.InitializeComponent();
this.ViewModel = new ViewModel();
}
}
MainPage.xaml
<Page>
<Page.Resources>
<DataTemplate x:Key="ListIntTemplate" x:DataType="local:VmServiceModel">
<ListView ItemsSource="{x:Bind Parameters}"
HorizontalAlignment="Center"
SelectionMode="None" Background="Transparent">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel VerticalAlignment="Top"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Parameter">
<TextBox Text="{Binding Value Mode=TwoWay}" Height="36" Width="65"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</Page.Resources>
<Grid>
<ListView ItemsSource="{x:Bind ViewModel.AtlasMethodParameterList}"
ItemTemplate="{StaticResource ListIntTemplate}">
</ListView>
</Grid>
</Page>
But when I change the values in the TextBox it dosen't update back the source that is the CurrentParameterValue property.
Binding in ListView doesn't know how to update the Property of type object because it's ItemsSource and it can update only ICollection such as you can't interact with object like List in C#. for example:
object MyList = new object();
MyList.Add("something"); // Compile error
And in my viewmodel the object which can be a list of long, list of double etc comes from an external API.
You need this solution then.
public class VmServiceMethodsViewDataGridModel : BindableBaseThreadSafe
{
private List<object> _currentParameterValue; // or ObservableCollection
public List<object> CurrentParameterValue
{
get => _currentParameterValue;
set => Set(ref _currentParameterValue, value);
}
}
Additionally
I have no idea what do you want to achieve or solve with this syntax
<ListView ItemsSource="{x:Bind ViewModel.AtlasMethodParameterList,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
Everything must work with this
<ListView ItemsSource="{Binding AtlasMethodParameterList}">
Mode=TwoWay is default Mode, you may not include it here explicitly.
UpdateSourceTrigger=PropertyChanged (Default is LostFocus) is needed in UI->VM direction, not in a back way. So, it's useless here. You may apply it to the TextBox in template instead.
EDIT
Because Two-way Binding requires explicit Path and the target must be a Property which contains Setter.
The workaround with your Demo app
<ListView Grid.Row="0"
ItemsSource="{Binding Demo.CurrentParameterValue}"
HorizontalAlignment="Center" VerticalAlignment="Center">
<ListView.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" Width="100"></TextBox>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private VmServiceMethodsViewDataGridModel _demo;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public VmServiceMethodsViewDataGridModel Demo
{
get => _demo;
set
{
_demo = value;
OnPropertyChanged("Demo");
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
Demo = new VmServiceMethodsViewDataGridModel();
Demo.CurrentParameterValue.Add(new MyItem { Value = 1 });
Demo.CurrentParameterValue.Add(new MyItem { Value = 2 });
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var collection = Demo.CurrentParameterValue;
MessageBox.Show(string.Format("Values are {0}, {1}", collection[0].Value, collection[1].Value));
}
}
// here it is
public class MyItem
{
public object Value { get; set; }
}
public class VmServiceMethodsViewDataGridModel : INotifyPropertyChanged
{
private List<MyItem> _currentParameterValue;
public List<MyItem> CurrentParameterValue
{
get => _currentParameterValue;
set
{
_currentParameterValue = value;
OnPropertyChanged("CurrentParameterValue");
}
}
public VmServiceMethodsViewDataGridModel()
{
CurrentParameterValue = new List<MyItem>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Additionally you may implement INPC for the Value regarding to your needs.

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));
}
}

WPF Binding ICommand with ViewModel

ICommand:
public class CMDAddEditUser : ICommand
{
public event EventHandler CanExecuteChanged;
public VMAddEditUser ViewModel { get; set;}
public CMDAddEditUser()
{
}
public CMDAddEditUser(VMAddEditUser vm)
{
ViewModel = vm;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
this.ViewModel.SimpleMethod();
}
}
ViewModel:
public class VMAddEditUser
{
private Employee _employee = new Employee();
private CMDAddEditUser Command { get; set; }
public VMAddEditUser()
{
Command = new CMDAddEditUser(this);
}
public string txtFirstName
{
get { return _employee.FirstName; }
set { _employee.FirstName = value; }
}
public void SimpleMethod()
{
txtFirstName = "abc";
}
}
XAML:
<Window x:Class="WPF.AddEditUserView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ViewModel;assembly=ViewModel"
Title="AddEditUserView" Height="392.329" Width="534.143">
<Grid Margin="0,0,2,-3">
<Grid.Resources>
<vm:VMAddEditUser x:Key="abx"/>
</Grid.Resources>
<Grid.DataContext>
<vm:VMAddEditUser/>
</Grid.DataContext>
<Button x:Name="btn" Content="Cancel" Command="{Binding SimpleMethod, Source={StaticResource abx}}"/>
</Grid>
</Window>
The CMDAddEditUser and VMAddEditUser is in the same project while the xaml is in a different project.
The .Execute(Object Parameter) of the ICommand doesn't seem to work. I can't bind the SimpleMethod with the button that I have. When I type the Command Binding in the xaml file, the auto-complete/suggestions only shows the txtFirstName and not the SimpleMethod. I can't figure out why the SimpleMethod can't be binded and can't be found. What did I do wrong in this code?
First: All properties you want your view to be able to bind to, must be public. Since view binds to property, which is instance of ICommand implementation, property must be public, so view can access it. However, your SimpleMethod() can be private if you don't wanna expose it to the outside world, that why you have command calling it instead of letting view directly call it.
Second: You set you grids DataContext to your 'VMEditUser' class, so in binding there is no need to specify Source, DataContext is source.

Recognizing checkbox created by a template in a fired event

I've created an unfoldable list of checkboxes like so.
<Expander x:Name=...>
<ListBox ItemsSource="{x:Static local:MainWindow.AllTypes}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}"
Checked="ToggleButton_OnToggled"
Unchecked="ToggleButton_OnToggled"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Expander>
I also have a method with the signature below.
private void FilterStuffOut(String condition)
{
CollectionViewSource source
= new CollectionViewSource { Source = dataGrid.ItemsSource };
ICollectionView view = source.View;
view.Filter = element => BringItOut(element, condition);
dataGrid.ItemsSource = view;
}
I'm unsure (and poking around with intellisense both in sender and eventArgs gave me nothing) how to get to know which checkbox is the firey one. Where should I look for it in the method below?
private void ToggleButton_OnToggled(
Object sender, RoutedEventArgs eventArgs) { ... }
You would typically write it like shown below, explicitly not using the as operator, but casting to the desired types. This is because you expect those types, and any other type should result in a runtime error, i.e. an InvalidCastException.
private void ToggleButton_OnToggled(object sender, RoutedEventArgs eventArgs)
{
var element = (FrameworkElement)sender;
var myType = (MyType)element.DataContext;
// do something with myType.MyValue
}
If you would need properties of more derived types, e.g. ToggleButton.IsChecked, you would use that type instead of FrameworkElement.
As promised.
I tend to use Movies for my examples, so please excuse my habit. I wasn't certain exactly what your UI looked like, or what you were trying to achieve here, but I think I understood the general idea.
If you're not familiar with the MVVM design pattern, I'd advise working through this tutorial.
Here we go.
These are my models:
//This is a base class which handles our notify property changed stuff which will update the UI
//when properties change.
public class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
The class above is the base class for all models and sometimes view models if the view model requires it. It's pretty useful and I'd advise that you implement something like this in all of your MVVM applications.
public class Filter : NotifyPropertyChangedBase
{
public event EventHandler OnEnabledChanged;
public string Genre { get; set; }
private bool _IsEnabled;
public bool IsEnabled
{
get { return _IsEnabled; }
set
{
_IsEnabled = value;
OnPropertyChanged();
if (OnEnabledChanged != null)
OnEnabledChanged(this, new EventArgs());
}
}
public Filter(string genre)
{
this.Genre = genre;
}
}
public class Movie
{
//We don't need to implement INotifyPropertyChanged here
//because these values will never change.
public string Name { get; set; }
public string Genre { get; set; }
}
The models above represent movies, and a filter for the genre of a movie. All models do not "do" anything, they simply represent data.
And here is the ViewModel:
public class MovieViewModel : NotifyPropertyChangedBase
{
private ObservableCollection<Movie> _FilteredMovies;
public ObservableCollection<Movie> FilteredMovies
{
get { return _FilteredMovies; }
set
{
_FilteredMovies = value;
//Need to implement INotifyPropertyChanged here because
//I am instantiating a new observable collection in the enabled changed
//method. This will refresh the binding on the DataGrid.
OnPropertyChanged();
}
}
public ObservableCollection<Movie> Movies { get; set; }
public ObservableCollection<Filter> Filters { get; set; }
public MovieViewModel()
{
this.Movies = new ObservableCollection<Movie>();
this.Filters = new ObservableCollection<Filter>();
#region Sample Data
this.Movies.Add(new Movie()
{
Name = "Movie Action",
Genre = "Action"
});
this.Movies.Add(new Movie()
{
Name = "Movie Romance",
Genre = "Romance"
});
this.Movies.Add(new Movie()
{
Name = "Movie Comedy",
Genre = "Comedy"
});
this.Filters.Add(new Filter("Action"));
this.Filters.Add(new Filter("Romance"));
this.Filters.Add(new Filter("Comedy"));
foreach (Filter filter in this.Filters)
filter.OnEnabledChanged += filter_OnEnabledChanged;
#endregion
}
void filter_OnEnabledChanged(object sender, EventArgs e)
{
var filteredMovies = (from m in this.Movies
join f in this.Filters on m.Genre equals f.Genre
where f.IsEnabled
select m).ToList();
this.FilteredMovies = new ObservableCollection<Movie>(filteredMovies);
}
}
It has a collection of movies, and a collection of filters. When a checkbox is selected, the OnEnabledChanged method will be called and set the FilteredMovies property to that of the selected filters. This in turn will call the notify property changed code and update the UI.
Here is the UI:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModels="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<ViewModels:MovieViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Expander>
<ItemsControl ItemsSource="{Binding Filters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsEnabled}"
Content="{Binding Genre}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
<DataGrid Grid.Row="1"
ItemsSource="{Binding FilteredMovies}"/>
</Grid>
Similar to your implementation, there is a DataGrid which is bound to the FilteredMovies property in the ViewModel, and the list of Filters is represented as a list of CheckBox objects.
Like I mentioned earlier, I wasn't exactly sure what you were trying to implement, but I think this was something of what you were trying to do.

Making AvalonEdit MVVM compatible

I'm trying to make Avalon MVVM compatible in my WPF application. From googling, I found out that AvalonEdit is not MVVM friendly and I need to export the state of AvalonEdit by making a class derived from TextEditor then adding the necessary dependency properties. I'm afraid that I'm quite lost in Herr Grunwald's answer here:
If you really need to export the state of the editor using MVVM, then I suggest you create a class deriving from TextEditor which adds the necessary dependency properties and synchronizes them with the actual properties in AvalonEdit.
Does anyone have an example or have good suggestions on how to achieve this?
Herr Grunwald is talking about wrapping the TextEditor properties with dependency properties, so that you can bind to them. The basic idea is like this (using the CaretOffset property for example):
Modified TextEditor class
public class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public static DependencyProperty CaretOffsetProperty =
DependencyProperty.Register("CaretOffset", typeof(int), typeof(MvvmTextEditor),
// binding changed callback: set value of underlying property
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.CaretOffset = (int)args.NewValue;
})
);
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
public new int CaretOffset
{
get { return base.CaretOffset; }
set { base.CaretOffset = value; }
}
public int Length { get { return base.Text.Length; } }
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Length");
base.OnTextChanged(e);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Now that the CaretOffset has been wrapped in a DependencyProperty, you can bind it to a property, say Offset in your View Model. For illustration, bind a Slider control's value to the same View Model property Offset, and see that when you move the Slider, the Avalon editor's cursor position gets updated:
Test XAML
<Window x:Class="AvalonDemo.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:avalonExt="clr-namespace:WpfTest.AvalonExt"
DataContext="{Binding RelativeSource={RelativeSource Self},Path=ViewModel}">
<StackPanel>
<avalonExt:MvvmTextEditor Text="Hello World" CaretOffset="{Binding Offset}" x:Name="editor" />
<Slider Minimum="0" Maximum="{Binding ElementName=editor,Path=Length,Mode=OneWay}"
Value="{Binding Offset}" />
<TextBlock Text="{Binding Path=Offset,StringFormat='Caret Position is {0}'}" />
<TextBlock Text="{Binding Path=Length,ElementName=editor,StringFormat='Length is {0}'}" />
</StackPanel>
</Window>
Test Code-behind
namespace AvalonDemo
{
public partial class TestWindow : Window
{
public AvalonTestModel ViewModel { get; set; }
public TestWindow()
{
ViewModel = new AvalonTestModel();
InitializeComponent();
}
}
}
Test View Model
public class AvalonTestModel : INotifyPropertyChanged
{
private int _offset;
public int Offset
{
get { return _offset; }
set
{
_offset = value;
RaisePropertyChanged("Offset");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
You can use the Document property from the editor and bind it to a property of your ViewModel.
Here is the code for the view :
<Window x:Class="AvalonEditIntegration.UI.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AvalonEdit="clr-namespace:ICSharpCode.AvalonEdit;assembly=ICSharpCode.AvalonEdit"
Title="Window1"
WindowStartupLocation="CenterScreen"
Width="500"
Height="500">
<DockPanel>
<Button Content="Show code"
Command="{Binding ShowCode}"
Height="50"
DockPanel.Dock="Bottom" />
<AvalonEdit:TextEditor ShowLineNumbers="True"
Document="{Binding Path=Document}"
FontFamily="Consolas"
FontSize="10pt" />
</DockPanel>
</Window>
And the code for the ViewModel :
namespace AvalonEditIntegration.UI
{
using System.Windows;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
public class ViewModel
{
public ViewModel()
{
ShowCode = new DelegatingCommand(Show);
Document = new TextDocument();
}
public ICommand ShowCode { get; private set; }
public TextDocument Document { get; set; }
private void Show()
{
MessageBox.Show(Document.Text);
}
}
}
source : blog nawrem.reverse
Not sure if this fits your needs, but I found a way to access all the "important" components of the TextEditor on a ViewModel while having it displayed on a View, still exploring the possibilities though.
What I did was instead of instantiating the TextEditor on the View and then binding the many properties that I will need, I created a Content Control and bound its content to a TextEditor instance that I create in the ViewModel.
View:
<ContentControl Content="{Binding AvalonEditor}" />
ViewModel:
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
// ...
private TextEditor m_AvalonEditor = new TextEditor();
public TextEditor AvalonEditor => m_AvalonEditor;
Test code in the ViewModel (works!)
// tests with the main component
m_AvalonEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");
m_AvalonEditor.ShowLineNumbers = true;
m_AvalonEditor.Load(#"C:\testfile.xml");
// test with Options
m_AvalonEditor.Options.HighlightCurrentLine = true;
// test with Text Area
m_AvalonEditor.TextArea.Opacity = 0.5;
// test with Document
m_AvalonEditor.Document.Text += "bla";
At the moment I am still deciding exactly what I need my application to configure/do with the textEditor but from these tests it seems I can change any property from it while keeping a MVVM approach.

Categories

Resources