WPF Binding ObservableCollection<T> T.Property to Window ViewModel in XAML - c#

I have inherited a class, MyModernWindow from Window, and added a property and dependency property called MyTitleLinks. The type is MyLinkCollection : ObservableCollection<MyLink>. In XAML, I'm trying to define the MyTitleLinks, and bind the MyLink.Command property to a property in my Window's ViewModel.
I have tried numerous ways to bind, including FindAncestor and ElementName, and I am constantly unsuccessful.
If using {Binding AboutCommand} or {Binding DataContext.AboutCommand, ElementName=mainWindow}, I get this error in the Output:
Cannot find governing FrameworkElement or FrameworkContentElement for target
element. BindingExpression:Path=AboutCommand; DataItem=null; target
element is 'MylLink' (HashCode=30245787); target property is 'Command'
(type 'ICommand')
If using {Binding DataContext.AboutCommand, RelativeSource={RelativeSource AncestorType={x:Type local:MyModernWindow}}},
Cannot find source for binding with reference 'RelativeSource
FindAncestor,
AncestorType='My.Namespace.MyModernWindow',
AncestorLevel='1''. BindingExpression:Path=DataContext.AboutCommand;
DataItem=null; target element is 'MyLink' (HashCode=35075009); target
property is 'Command' (type 'ICommand')
MainWindow.xaml
<local:MyModernWindow x:Class="My.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:My.Controls"
IsTitleVisible="True"
Style="{StaticResource MyModernWindow}"
Title="My Window"
WindowStartupLocation="CenterScreen">
<local:MyModernWindow.MyTitleLinks>
<local:MyLink DisplayName="Support" Source="https://www.google.com/support/" />
<local:MyLink DisplayName="About" Command="{Binding AboutCommand}" />
</local:MyModernWindow.MyTitleLinks>
</local:MyModernWindow>
MainWindow.xaml.cs
public partial class MainWindow : MyModernWindow
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
MyLinkCollection Class
public class MyLinkCollection : ObservableCollection<MyLink>
{
}
MyLink Class
public class MyLink : DependencyObject
{
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(MyLink));
public static readonly DependencyProperty DisplayNameProperty = DependencyProperty.Register(nameof(DisplayName), typeof(string), typeof(MyLink));
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(Uri), typeof(MyLink));
public Uri Source
{
get { return (Uri)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public string DisplayName
{
get { return (string)GetValue(DisplayNameProperty); }
set { SetValue(DisplayNameProperty, value); }
}
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public MyLink()
{
SetCurrentValue(VisibilityProperty, Visibility.Visible);
}
}
ViewModel
public class MainWindowViewModel
{
public ICommand AboutCommand { get; private set; }
public MainWindowViewModel()
{
this.AboutCommand = new RelayCommand(OpenAboutWindow);
}
private void OpenAboutWindow(object o)
{
ModernDialog.ShowMessage("About Screen", "About", MessageBoxButton.OK);
}
}
What am I missing?

With the help of this blog post, I figured it out. Since MyLink and MyLinkCollection aren't in the visual tree, I used a "Proxy Element" to give a context.
I gave my Window a name, created a FrameworkElement, then created a hidden ContentControl. That's all I needed.
Here's the working XAML:
<local:MyModernWindow x:Class="My.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:My.Controls"
x:Name="Window"
IsTitleVisible="True"
Style="{StaticResource MyModernWindow}"
Title="My Window"
WindowStartupLocation="CenterScreen">
<local:MyModernWindow.Resources>
<FrameworkElement x:Key="ProxyElement" DataContext="{Binding DataContext, ElementName=Window}" />
</local:MyModernWindow.Resources>
<ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>
<local:MyModernWindow.MyTitleLinks>
<local:MyLink DisplayName="Support" Source="{Binding DataContext.SupportSource, Source={StaticResource ProxyElement}}" />
<local:MyLink DisplayName="About" Command="{Binding DataContext.AboutCommand, Source={StaticResource ProxyElement}}" />
</local:MyModernWindow.MyTitleLinks>
</local:MyModernWindow>

The reason for the problem is that the DataContext is not inherited from the collection nor from the MyLink item.
To have WPF automatically managing the inheritance for you without the need of a proxy element you need to add "Freezable" at each step of your tree as follows:
public class MyLinkCollection : FreezableCollection<MyLink>
{
}
and
public class MyLink : Freezable
{
// class body
}
Xaml Behaviors Wpf(a Microsoft released project) uses the same approach to propagate the DataContext inside a Xaml defined collection without the need of additional proxies

Related

How to pass commands across different XAML files/controls

I come from a WPF background so I thought I'd experiment with building a to-do app in WinUI 3. The app structure is a little overdesigned as I'm trying build it out like a more complex app. For that reason I have a ToDoTaskView and ToDoTaskViewModel, along with a MainWindowView and MainWindowViewModel even though it'd be much easier to build the entire app in a single XAML file.
The ToDoTaskView has a delete button, but the delete command lives on the MainWindowViewModel, as that's where the list that it must be deleted from lives. I think this a pretty common pattern where a sub-view needs to send a command to a parent view model.
The (abridged) MainWindowView:
<UserControl>
<ItemsRepeater ItemsSource="{Binding Tasks}">
<DataTemplate>
<local:ToDoTaskView />
</DataTemplate>
</ItemsRepeater>
</UserControl>
And the (heavily abridged) ToDoTaskView:
<UserControl>
<Button Command="???">Delete</Button>
</UserControl>
In WPF there's many ways to deal with this.
RoutedCommand
My prefered method. The MainWindowView can listen for a custom ToDoTaskDeleted routed command and bind to the command on the view model. Then any UI element anywhere underneath MainWindowView can fire said event and rest easy knowing it'll be handled somewhere above it on the visual tree.
There's no RoutedCommand in WinUI 3, and even worse, routed events are locked down and you can't define custom ones. So even building a custom RoutedCommand implementation would be difficult.
DynamicResource
I can define a StandardUICommand in MainWindowView.Resources, bind it to the command in the view model, then in ToDoTaskView I can use {DynamicResource DeleteCommand} to have the resource system search up the visual tree for the command.
Except I can't. WinUI3 doesn't have DynamicResource, only StaticResource. And since the two views are in different XAML files, and ToDoTaskView in a templated context, StaticResource can't resolve the resource name between them.
I think this could work for resources in App.xaml, but I'd rather not shove every command into the top level scope instead of keeping them where they belong.
All the commanding examples in the Microsoft docs seem to assume that the button and handler are in the same file, or they directly pass a reference to the command through to the child view's DataContext.
RelativeAncestor
Peter below reminded me that I tried this too, and found it's missing in WinUI 3. RelativeSource doesn't support any kind of ancestor discovery.
Manual Kludge
Setting up a direct reference from ToDoTaskViewModel to MainWindowViewModel is certainly possible, but I hate it. After all, who's to guarantee that this particular to do item is part of a list at any one moment? Maybe it lives in a pop-up dialog as a reminder? Handling this kind of thing through the visual tree is the Correct(tm) way to do it.
I wouldn't accept a PR from a coworker on my WPF project with this solution. But I can't seem to find any better way in WinUI 3.
Have I missed something about WinUI 3? Is it just not mature enough yet to have a solution? It seems like this scenario isn't so uncommon that it would be completely unsupported.
In this case, I'd create an ICommand dependency property, DeleteCommand and and bind a command in the view model. Here's a sample code using the CommunityToolkit.Mvvm NuGet package.
MainWindow.xaml
The MainWindow is named, "ThisWindow" in this case, so we can access its ViewModel from the ItemTemplate.
The DeleteCommandParameter is bound to the DataContext of the item, ToDoTaskViewModel in this case.
<Window
x:Class="ToDoApp.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:local="using:ToDoApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="ThisWindow"
mc:Ignorable="d">
<Grid RowDefinitions="Auto,*">
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<TextBox x:Name="NewToDo" />
<Button
Command="{x:Bind ViewModel.AddToDoCommand}"
CommandParameter="{x:Bind NewToDo.Text, Mode=OneWay}"
Content="Add" />
</StackPanel>
<ScrollViewer Grid.Row="1">
<ItemsRepeater ItemsSource="{x:Bind ViewModel.ToDoTasks, Mode=OneWay}">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="local:ToDoTaskViewModel">
<local:ToDoTaskView
DeleteCommand="{Binding ElementName=ThisWindow, Path=ViewModel.DeleteToDoCommand}"
DeleteCommandParameter="{x:Bind}"
ToDo="{x:Bind ToDo, Mode=OneWay}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</Window>
MainWindow.xaml.cs
using Microsoft.UI.Xaml;
namespace ToDoApp;
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
public MainWindowViewModel ViewModel { get; } = new();
}
MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
namespace ToDoApp;
[ObservableObject]
public partial class MainWindowViewModel
{
[ObservableProperty]
private ObservableCollection<ToDoTaskViewModel> toDoTasks = new();
[RelayCommand]
private void AddToDo(string todo)
{
ToDoTasks.Add(new ToDoTaskViewModel() { ToDo = todo });
}
[RelayCommand]
private void DeleteToDo(ToDoTaskViewModel toDoTask)
{
ToDoTasks.Remove(toDoTask);
}
}
ToDoTaskView.xaml
<UserControl
x:Class="ToDoApp.ToDoTaskView"
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:local="using:ToDoApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid ColumnDefinitions="*,Auto">
<TextBlock
Grid.Column="0"
Text="{x:Bind ToDo, Mode=OneWay}" />
<Button
Grid.Column="1"
Command="{x:Bind DeleteCommand, Mode=OneWay}"
CommandParameter="{x:Bind DeleteCommandParameter, Mode=OneWay}"
Content="Delete" />
</Grid>
</UserControl>
ToDoTaskView.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Windows.Input;
namespace ToDoApp;
public sealed partial class ToDoTaskView : UserControl
{
public static readonly DependencyProperty ToDoProperty = DependencyProperty.Register(
nameof(ToDo),
typeof(string),
typeof(ToDoTaskView),
new PropertyMetadata(default));
public static readonly DependencyProperty DeleteCommandProperty = DependencyProperty.Register(
nameof(DeleteCommand),
typeof(ICommand),
typeof(ToDoTaskView),
new PropertyMetadata(default));
public static readonly DependencyProperty DeleteCommandParameterProperty = DependencyProperty.Register(
nameof(DeleteCommandParameter),
typeof(object),
typeof(ToDoTaskView),
new PropertyMetadata(default));
public ToDoTaskView()
{
this.InitializeComponent();
}
public string ToDo
{
get => (string)GetValue(ToDoProperty);
set => SetValue(ToDoProperty, value);
}
public ICommand DeleteCommand
{
get => (ICommand)GetValue(DeleteCommandProperty);
set => SetValue(DeleteCommandProperty, value);
}
public object DeleteCommandParameter
{
get => (object)GetValue(DeleteCommandParameterProperty);
set => SetValue(DeleteCommandParameterProperty, value);
}
}
ToDoTaskViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ToDoApp;
[ObservableObject]
public partial class ToDoTaskViewModel
{
[ObservableProperty]
private string toDo = string.Empty;
}
Ok I have a solution. I cannot emphasize enough how much of a disgusting hack this is. Normally I'd be embarrassed to post this, but the only ones who should be embarrassed are Microsoft for publishing Win UI 3 in its current state and claiming it's capable of making real applications.
The gist of this is to mimic Ancestor-type RelativeSource binding in WPF. We create two attached properties - ParentContextViewType to specify the type of the ancestor we're looking for - and ParentContextView which is automatically assigned a reference to the desired parent view instance when the child loads. (I'd have made ParentContextView a readonly property, but of course, Win UI doesn't support that...) Then for the child button, we do a RelativeSource Self binding to the attached ParentContextView property, then adding the rest of the path, just like we would with a legit ancestor type bind.
Here goes (and may god have mercy on my soul):
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
namespace ParentBinding
{
public static class Hacks
{
public static DependencyProperty ParentContextView =
DependencyProperty.RegisterAttached(
"ParentContextView",
typeof(FrameworkElement),
typeof(Hacks),
new PropertyMetadata(null));
public static FrameworkElement GetParentContextView(DependencyObject d)
{
return d.GetValue(ParentContextView) as FrameworkElement;
}
public static void SetParentContextView(DependencyObject d, FrameworkElement view)
{
d.SetValue(ParentContextView, view);
}
public static DependencyProperty ParentContextViewTypeProperty =
DependencyProperty.RegisterAttached(
"ParentContextViewType",
typeof(Type),
typeof(Hacks),
new PropertyMetadata(null, (d, e) =>
{
if (!(d is FrameworkElement fe))
return;
if (e.OldValue != null)
fe.Loaded -= OnParentContextFeLoaded;
if (e.NewValue != null)
fe.Loaded += OnParentContextFeLoaded;
}));
private static void OnParentContextFeLoaded(object sender, RoutedEventArgs e)
{
if (!(sender is FrameworkElement fe))
return;
var type = GetParentContextViewType(fe);
if (type == null)
return;
while (!type.IsAssignableFrom(fe.GetType()) &&
(fe = VisualTreeHelper.GetParent(fe) as FrameworkElement) != null)
{
}
SetParentContextView(sender as DependencyObject, fe);
}
public static Type GetParentContextViewType(DependencyObject d)
{
return d.GetValue(ParentContextViewTypeProperty) as Type;
}
public static void SetParentContextViewType(DependencyObject d, Type val)
{
d.SetValue(ParentContextViewTypeProperty, val);
}
}
}
A use-case:
Model stuff:
using Microsoft.UI.Xaml.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace ParentBinding
{
public class Command : ICommand
{
Action<object> _action;
public Command(Action<object> action)
{
_action = action;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter)
{
_action?.Invoke(parameter);
}
}
public class Parent
{
public ObservableCollection<Child> Children { get; set; }
private Command _deleteChildCommand;
public ICommand DeleteChildCommand =>
_deleteChildCommand ?? (_deleteChildCommand = new Command((p) =>
{
if (!(p is Child ch))
return;
this.Children.Remove(ch);
}));
}
public class Child
{
public string Name { get; set; }
public override string ToString() => this.Name;
}
}
Main Window:
<Window x:Class="ParentBinding.MainWindow"
x:Name="_main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ParentBinding"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ListView DataContext="{Binding ElementName=_main, Path=Parent}"
ItemsSource="{Binding Children}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Child">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" />
<Button local:Hacks.ParentContextViewType="ListView"
Grid.Column="1"
CommandParameter="{Binding}"
Content="Delete"
Command="{Binding
Path=(local:Hacks.ParentContextView).DataContext.DeleteChildCommand,
RelativeSource={RelativeSource Self}}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Window>
using Microsoft.UI.Xaml;
namespace ParentBinding
{
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
public Parent Parent { get; } = new Parent
{
Children = new System.Collections.ObjectModel.ObservableCollection<Child>
{
new Child
{
Name = "Larry"
},
new Child
{
Name = "Curly"
},
new Child
{
Name = "Moe"
}
}
};
}
}
Amazingly, it works, and one of the reasons I was so curious to try it and post it is that it is, more or less, a general purpose substitute for ancestor type binding in WinUI 3. Hope someone finds it useful.

What's the issue with the way I'm binding to a dependency property?

Note: You can find the project below on github now. https://github.com/ReasonSharp/MyTestRepo
I'm creating a simple list control with a scrollbar that will display a collection of objects I pass to it. When a user clicks on one item, I want it to become a selected item, and when he clicks it again, I want it to be unselected. I store the selected item in a SelectedLocation property. While debugging, the property is set appropriately. However, if I place this list control (LocationListView) onto a window and bind to SelectedLocation (like SelectedLocation="{Binding MyLocation}") in a control, the binding won't work, and if I try to use this MyLocation in another binding in the same window (i.e. <TextBox Text="{Binding MyLocation.ID}"/>, where ID is a dependency property), that binding won't show anything changing as I select different items in the list.
Minimal example is a bit large, please bear with me:
List control
XAML
<UserControl x:Class="MyListView.LocationListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MyListView"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="locationListView">
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="myStackPanel"/>
</ScrollViewer>
</Grid>
</UserControl>
Code behind
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace MyListView {
public partial class LocationListView : UserControl {
#region Dependency Properties
public IEnumerable Locations {
get { return (IEnumerable)GetValue(LocationsProperty); }
set { SetValue(LocationsProperty, value); }
}
public static readonly DependencyProperty LocationsProperty =
DependencyProperty.Register("Locations", typeof(IEnumerable), typeof(LocationListView), new PropertyMetadata(null, LocationsChanged));
public MyObject SelectedLocation {
get { return (MyObject)GetValue(SelectedLocationProperty); }
set { SetValue(SelectedLocationProperty, value); }
}
public static readonly DependencyProperty SelectedLocationProperty =
DependencyProperty.Register("SelectedLocation", typeof(MyObject), typeof(LocationListView), new PropertyMetadata(null));
#endregion
private static void LocationsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
((LocationListView)o).RegenerateLocations();
if (((LocationListView)o).Locations is ObservableCollection<MyObject>) {
var l = ((LocationListView)o).Locations as ObservableCollection<MyObject>;
l.CollectionChanged += ((LocationListView)o).L_CollectionChanged;
}
}
private void L_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
RegenerateLocations();
}
private Button selectedLV = null;
public LocationListView() {
InitializeComponent();
}
private void RegenerateLocations() {
if (Locations != null) {
myStackPanel.Children.Clear();
foreach (var l in Locations) {
var b = new Button();
b.Content = l;
b.Click += B_Click;
myStackPanel.Children.Add(b);
}
}
selectedLV = null;
}
private void B_Click(object sender, RoutedEventArgs e) {
var lv = (sender as Button)?.Content as MyObject;
if (selectedLV != null) {
lv.IsSelected = false;
if ((selectedLV.Content as MyObject) == SelectedLocation) {
SelectedLocation = null;
selectedLV = null;
}
}
if (lv != null) {
SelectedLocation = lv;
selectedLV = sender as Button;
lv.IsSelected = true;
}
}
}
}
Note the absence of this.DataContext = this; line. If I use it, I get the following binding expression path errors:
System.Windows.Data Error: 40 : BindingExpression path error: 'SillyStuff' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=SillyStuff; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'Locations' (type 'IEnumerable')
System.Windows.Data Error: 40 : BindingExpression path error: 'MySelectedLocation' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=MySelectedLocation; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'SelectedLocation' (type 'MyObject')
Using (this.Content as FrameworkElement).DataContext = this; won't produce these errors, but it won't work either.
Main window
XAML
<Window x:Class="MyListView.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:MyListView"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DockPanel LastChildFill="True" HorizontalAlignment="Stretch" VerticalAlignment="Top">
<local:LocationListView Locations="{Binding SillyStuff}" SelectedLocation="{Binding MySelectedLocation}" DockPanel.Dock="Top"/>
<TextBox Text="{Binding MySelectedLocation.ID}" DockPanel.Dock="Top"/>
</DockPanel>
</Grid>
</Window>
Code behind
using System.Windows;
using Microsoft.Practices.Unity;
namespace MyListView {
public partial class MainWindow : Window {
private MainViewModel vm;
public MainWindow() {
InitializeComponent();
}
[Dependency] // Unity
internal MainViewModel VM {
set {
this.vm = value;
this.DataContext = vm;
}
}
}
}
MainViewModel
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace MyListView {
class MainViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (PropertyChanged != null)
PropertyChanged(sender, e);
}
private MyObject mySelectedLocation;
public MyObject MySelectedLocation {
get { return mySelectedLocation; }
set {
mySelectedLocation = value;
OnPropertyChanged(this, new PropertyChangedEventArgs("MySelectedLocation"));
}
}
public ObservableCollection<MyObject> SillyStuff {
get; set;
}
public MainViewModel() {
var cvm1 = new MyObject();
cvm1.ID = 12345;
var cvm2 = new MyObject();
cvm2.ID = 54321;
var cvm3 = new MyObject();
cvm3.ID = 15243;
SillyStuff = new ObservableCollection<MyObject>();
SillyStuff.Add(cvm1);
SillyStuff.Add(cvm2);
SillyStuff.Add(cvm3);
}
}
}
MyObject
using System.Windows;
namespace MyListView {
public class MyObject : DependencyObject {
public int ID {
get { return (int)GetValue(IDProperty); }
set { SetValue(IDProperty, value); }
}
public static readonly DependencyProperty IDProperty =
DependencyProperty.Register("ID", typeof(int), typeof(MyObject), new PropertyMetadata(0));
public bool IsSelected {
get; set;
}
public override string ToString() {
return ID.ToString();
}
}
}
App.xaml -- just to save anyone the typing
XAML
<Application x:Class="MyListView.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyListView">
<Application.Resources>
</Application.Resources>
</Application>
Code behind
using System.Windows;
using Microsoft.Practices.Unity;
namespace MyListView {
public partial class App : Application {
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
UnityContainer container = new UnityContainer();
var mainView = container.Resolve<MainWindow>();
container.Dispose();
mainView.Show();
}
}
}
The objective here is to have the value in the TextBox on MainWindow change to the selected item's ID whenever the selected item changes. I could probably do it by creating a SelectedItemChanged event on my LocationListView, and then setting the property manually in a handler, but that seems like a hack. If you place a <ListView ItemsSource="{Binding SillyStuff}" SelectedItem="{Binding MySelectedLocation}" DockPanel.Dock="Top"/> instead of my list control, this works like a charm, so I should be able to make my control work that way too.
Edit: Changed MainViewModel to implement INotifyPropertyChanged as per Pieter's instructions.
Main issues
When you select an item in your custom control, B_Click assigns it to the SelectedLocation property, which calls SetValue internally. However, this overwrites the binding on SelectedLocation - in other words, after that call SelectedLocation is no longer bound to anything. Use SetCurrentValue instead to preserve the binding.
However, bindings won't update their source by default. You'll have to set their Mode to TwoWay. You can do that in XAML: SelectedLocation="{Binding MySelectedLocation, Mode=TwoWay}", or mark the dependency property to use TwoWay binding by default: new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, LocationsChanged).
Finally, make sure that your binding paths are correct. Your text box binds to SelectedLocation, while the property is named MySelectedLocation. These kind of issues are usually logged in the debug output, in this case you should get a message like this:
System.Windows.Data Error: 40 : BindingExpression path error: 'SelectedLocation' property not found on 'object' ''MainViewModel' (HashCode=8757408)'. BindingExpression:Path=SelectedLocation.ID; DataItem='MainViewModel' (HashCode=8757408); target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
Other issues
I've found a few other issues as well: you're not unregistering L_CollectionChanged when another collection is set, and if the collection is removed, you're not clearing the visible items. The code in B_Click is also troublesome: you're also accessing lv before making sure it's not null, and if the user clicks on an unselected button you're setting SelectedLocation to null before setting it to the newly selected item. Also, when regenerating items, selectedLV (what's 'lv'?) is set to null, but SelectedLocation is left intact...
Also a little tip: your OnPropertyChanged method only needs a single argument: string propertyName. Make it optional and mark it with a [CallerMemberName] attribute, so all that a property setter needs to do is call it without arguments. The compiler will insert the calling property name for you.
Alternatives
Personally, I'd just use a ListView with a custom ItemTemplate:
<ListView ItemsSource="{Binding MyLocations}" SelectedItem="{Binding MySelectedLocation}" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<ToggleButton IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}" Content="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This probably requires a few more modifications to make it look nice, but that's the gist of it. Alternately, you could create an attached behavior that takes care of your desired selection behavior.
Oh boy, that's a lot of code.
Let me begin by highlighting a common mistake, which is setting the control's DataContext to itself. This should be avoided as it tends to screw up absolutely everything.
So. Avoid doing this:
this.DataContext = this;
It is not the responsibility of the UserControl itself to set it's own DataContext, it should be the responsibility of the parent control (such as a Window to set it. Like this:
<Window ...>
<local:MyUserControl DataContext="{Binding SomeProperty}" ... />
If your UserControl was to set its own DataContext, then it will override what the Window sets its DataContext to be. Which will result in the screwing up of absolutely everything.
To bind to a Dependency Property of a UserControl, simply give your control an x:Name and use an ElementName binding, like this:
<UserControl ...
x:Name="usr">
<TextBlock Text="{Binding SomeDependencyProperty, ElementName=usr}" ... />
What's important to note here is that the DataContext isn't being set at all, so your parent Window is free to set the control's DataContext to whatever it needs to be.
Adding to this, your UserControl can now bind to it's DataContext using a straightforward Path binding.
<UserControl ...
x:Name="usr">
<TextBlock Text="{Binding SomeDataContextProperty}" ... />
I hope this helps.

C# VS : Factoring code into UserControl, using ObservableCollection, and consuming it with Binding

I am factoring some code into UserControls which parameters are bound when consumed. I am meeting difficulties with the use of ObservableCollection as a DependencyProperty.
The example showing the difficulty is a project consisting in a MainWindow with two DependencyProperty:
one representing a String (named "Data") and
another one representing an ObservableCollection (named "Origin");
and a UserControl (named UserControl1) exposing two similar DependencyProperty (named resp. "Liste" and "Noun").
The MainWindow contains a TextBlock which Text is bound to "Data" and a ComboBox which ItemsSource is bound to "Origin". Both are working fine.
Both controls are factored into UserControl1, with the DependencyProperty "Liste" and "Noun" acting as intermediate, and UserControl1 is consumed in MainWindow.
Each DataContext (of MainWindow and of UserControl1) is set to "this".
The trouble is while the factored TextBlock (within UserControl1) is working and showing the content of "Data", the factored ComboBox is not working and its DropDown is empty.
The code of MainWindow.xaml is:
<Window x:Class="ChainedBindingUserControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="350" Width="525"
xmlns:Local="clr-namespace:ChainedBindingUserControl"
>
<StackPanel>
<TextBlock Text="{Binding Data}"
Width="150"
/>
<ComboBox ItemsSource="{Binding Origin}"
Width="150"
/>
<Label Content="--------------------------------------------------"
Width="200"
/>
<Local:UserControl1 Liste="{Binding Origin}"
Noun="{Binding Data}"
Height="50" Width="150"
/>
</StackPanel>
</Window>
Its code behind is :
namespace ChainedBindingUserControl
{
public partial class MainWindow : Window
{
public ObservableCollection<String> Origin
{
get { return (ObservableCollection<String>)GetValue(OriginProperty); }
set { SetValue(OriginProperty, value); }
}
public static readonly DependencyProperty OriginProperty =
DependencyProperty.Register("Origin", typeof(ObservableCollection<String>), typeof(MainWindow),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
public String Data
{
get { return (String)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(String), typeof(UserControl1),
new FrameworkPropertyMetadata("Blablabla", FrameworkPropertyMetadataOptions.AffectsRender));
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
ObservableCollection<String> zog = new ObservableCollection<String>();
zog.Add("A");
zog.Add("B");
zog.Add("C");
Origin = zog;
}
}
}
The file UserControl1.xaml is :
<UserControl x:Class="ChainedBindingUserControl.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Name="root"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel>
<TextBlock Text="{Binding Noun}"
/>
<ComboBox ItemsSource="{Binding Liste}"
/>
</StackPanel>
</UserControl>
Its code behind is :
namespace ChainedBindingUserControl
{
public partial class UserControl1 : UserControl
{
public ObservableCollection<String> Liste
{
get { return (ObservableCollection<String>)GetValue(ListeProperty); }
set { SetValue(ListeProperty, value); }
}
public static readonly DependencyProperty ListeProperty =
DependencyProperty.Register("Liste", typeof(ObservableCollection<String>), typeof(UserControl1),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
public String Noun
{
get { return (String)GetValue(NounProperty); }
set { SetValue(NounProperty, value); }
}
public static readonly DependencyProperty NounProperty =
DependencyProperty.Register("Noun", typeof(String), typeof(UserControl1),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsRender));
public UserControl1()
{
InitializeComponent();
this.DataContext = this;
}
}
}
`
EDIT
According to the pieces of information and snippets provided on http://sshumakov.com/2012/11/13/how-to-create-dependency-properties-for-collections/ , I changed the code behind of UserControl1 into
public partial class UserControl1 : UserControl
{
public IList Liste
{
get { return (List<String>)GetValue(ListeProperty); }
set { SetValue(ListeProperty, value); }
}
public static readonly DependencyProperty ListeProperty =
DependencyProperty.Register("Liste", typeof(IList), typeof(UserControl1),
new FrameworkPropertyMetadata(new List<String>(), FrameworkPropertyMetadataOptions.AffectsRender));
public String Noun
{
get { return (String)GetValue(NounProperty); }
set { SetValue(NounProperty, value); }
}
public static readonly DependencyProperty NounProperty =
DependencyProperty.Register("Noun", typeof(String), typeof(UserControl1),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsRender));
public UserControl1()
{
InitializeComponent();
this.DataContext = this;
SetValue(ListeProperty, new List<String>());
}
}
but it is still not working.
The trouble doesn't come from the DataContext since the TextBlock works as expected.
The trouble here is specific: why a DependecyProperty acting as an intermediate for Binding is working when the property is of type String while it doesn't work when it is of type ObservableCollection (or List, etc).
Thanks in advance for any explanation.
Your problem is in the UserControl's xaml, here:
<TextBlock Text="{Binding Noun}"
/>
<ComboBox ItemsSource="{Binding Liste}"
/>
These binding expressions are attempting to locate Noun and Liste properties on the DataContext of your UserControl, not on the UserControl itself. You need to specify a different target. Since you've already named your UserControl element, you can replace the bindings with this:
<TextBlock Text="{Binding ElementName=root, Path=Noun}"
/>
<ComboBox ItemsSource="{Binding ElementName=root, Path=Liste}"
/>
Imagine that you are creating control that has property that accepts collection:
public class CustomControl : Control
{
public IEnumerable<string> Items { get; set; }
}
If you want property Items to act as binding target you must change it to be dependency property:
public class CustomControl : Control
{
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register("Items", typeof(IEnumerable<string>), typeof (CustomControl), new PropertyMetadata(new List<string>()));
public IEnumerable<string> Items
{
get { return (IEnumerable<string>) GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
}
As you can see, we changed this property to dependency property and supplied new instance of List class as default parameter. As it turned out, this default value will be used on class level (i.e. it will be created only once and each instance of CustomControl will have reference to the same collection). Therefore, we need one modification:
public class CustomControl : Control
{
public CustomControl()
{
Items = new List<string>();
}
}
Now you can use this control and supply value for Items property via binding:
<Grid>
<DependencyPropertiesCollection:CustomControl Items="{Binding ItemsSource}"/>
</Grid>
Currently this control has one limitation – Items property can’t be filled directly in XAML like this code does:
<Grid>
<DependencyPropertiesCollection:CustomControl>
<DependencyPropertiesCollection:CustomControl.Items>
<System:String>Item 1</System:String>
<System:String>Item 2</System:String>
<System:String>Item 3</System:String>
<System:String>Item 4</System:String>
<System:String>Item 5</System:String>
</DependencyPropertiesCollection:CustomControl.Items>
</DependencyPropertiesCollection:CustomControl>
</Grid>
To fix this, you need to change property type from IEnumerable to IList:
public class CustomControl : Control
{
public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof (IList), typeof (CustomControl), new PropertyMetadata(new List<string>()));
public IList Items
{
get { return (IList)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
public CustomControl()
{
Items = new List<string>();
}
}
Credits:-
http://sshumakov.com/2012/11/13/how-to-create-dependency-properties-for-collections/

My WPF custom control's Data Context is superseding parent's

In my main window, I try to bind to a bool, but it's looking in my custom control's DataContext instead. If I don't assign DataContext in the user control, then the main window's bindings works, but (obviously) this brakes the bindings in the user control.
Here's the error:
System.Windows.Data Error: 40 : BindingExpression path error: 'MyControlVisible' property not found on 'object' ''MyUserControlModel' (HashCode=1453241)'. BindingExpression:Path=MyControlVisible; DataItem='MyUserControlModel' (HashCode=1453241); target element is 'MyUserControl' (Name='_myUserControl'); target property is 'Visibility' (type 'Visibility')
I need binding to work on both controls, but I don't want the user control's DataContext to supersede the window's.
Here's the code:
<Window x:Class="Sandbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="clr-namespace:Sandbox.Controls" Title="Sandbox">
<DockPanel LastChildFill="True">
<DockPanel.Resources>
<BooleanToVisibilityConverter x:Key="boolToVis" />
</DockPanel.Resources>
<Grid>
<Controls:MyUserControl x:Name="_myUserControl" Visibility="{Binding MyControlVisible, Converter={StaticResource boolToVis}}"/>
</Grid>
</DockPanel>
</Window>
namespace Sandbox
{
public partial class MainWindow
{
private MainWindowModel model;
public MainWindow()
{
InitializeComponent();
DataContext = model = new MainWindowModel();
_myUserControl.Initialize(model.MyUControlModel);
}
}
}
using System.ComponentModel;
using Sandbox.Controls;
namespace Sandbox
{
public class MainWindowModel : BaseModel
{
public MyUserControlModel MyUControlModel { get; set; }
public bool MyControlVisible { get; set; }
public MainWindowModel()
{
MyUControlModel = new MyUserControlModel();
MyControlVisible = false;
OnChange("");
}
}
public class BaseModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnChange(string s)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(s));
}
}
}
}
<UserControl x:Class="Sandbox.Controls.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d">
<Grid>
<TextBlock Text="{Binding MyBoundText}"/>
</Grid>
</UserControl>
namespace Sandbox.Controls
{
public partial class MyUserControl
{
public MyUserControl()
{
InitializeComponent();
}
public void Initialize(MyUserControlModel context)
{
DataContext = context;
}
}
}
namespace Sandbox.Controls
{
public class MyUserControlModel : BaseModel
{
public string MyBoundText { get; set; }
public MyUserControlModel()
{
MyBoundText = "Hello World!";
OnChange("");
}
}
}
That is one of the many reasons you should never set the DataContext directly from the UserControl itself.
When you do so, you can no longer use any other DataContext with it because the UserControl's DataContext is hardcoded in.
In the case of your binding, normally the DataContext would be inherited so the Visibility binding could find the property MyControlVisible on the current DataContext, however because you hardcoded the DataContext in your UserControl's constructor, that property is not found.
You could specify a different binding source in your binding, such as
<Controls:MyUserControl Visibility="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}},
Path=DataContext.MyControlVisible,
Converter={StaticResource boolToVis}}" ... />
However that's just a workaround for the problem for this specific case, and in my view is not a permanent solution. A better solution is to simply not hardcode the DataContext in your UserControl
There are a few different ways you can do depending on your UserControl's purpose and how your application is designed.
You could create a DependencyProperty on your UserControl to pass in the value, and bind to that.
<Controls:MyUserControl UcModel="{Binding MyUControlModelProperty}" ... />
and
<UserControl x:Class="Sandbox.Controls.MyUserControl"
ElementName=MyUserControl...>
<Grid DataContext="{Binding UCModel, ElementName=MyUserControl}">
<TextBlock Text="{Binding MyBoundText}"/>
</Grid>
</UserControl>
Or you could build your UserControl with the expectation that a specific property will get passed to it in the DataContext. This is normally what I do, in combination with DataTemplates.
<Controls:MyUserControl DataContext="{Binding MyUControlModelProperty}" ... />
and
<UserControl x:Class="Sandbox.Controls.MyUserControl"...>
<Grid>
<TextBlock Text="{Binding MyBoundText}"/>
</Grid>
</UserControl>
As I said above, I like to use DataTemplates to display my UserControls that expect a specific type of Model for their DataContext, so typically my XAML for the main window would look something like this:
<DataTemplate DataType="{x:Type local:MyUControlModel}">
<Controls:MyUserControl />
</DataTemplate>
<ContentPresenter Content="{Binding MyUControlModelProperty}" ... />

WPF Binding itemscontrol child controls

I have a user control that I'm using across several pages which defines a header label and two buttons. I want the control to be able to have child controls, but I ran into the problem of binding those child controls since they are in an itemscollection. When I add bindings to the child controls in XAML they are not registered.
Error output: System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=MyPage'. BindingExpression:Path=MyText; DataItem=null; target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
Excess code omitted for brevity.
e.g.
XAML
<UserControl x:Class="MyControl" Name="MyControl">
<Grid>
<ItemsControl Name="ItemsControl" ItemsSource="{Binding ItemsSource, ElementName=MyControl}" />
</Grid>
</UserControl>
Code Behind
[ContentProperty("Items")]
public partial class MyControl : UserControl
{
public static readonly DependencyProperty ItemsSourceProperty =
ItemsControl.ItemsSourceProperty.AddOwner(typeof (MyControl));
public IEnumerable ItemsSource
{
get { return (IEnumerable) GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public ItemCollection Items
{
get { return ItemsControl.Items; }
}
}
Usage:
<Page x:Class="MyPage" Name="MyPage">
<MyControl>
<TextBox Text="{Binding MyText,
ElementName=MyPage,
UpdateSourceTrigger=PropertyChanged}" />
</MyControl>
</Page>
Code Behind
public partial class MyPage : Page, INotifyPropertyChanged
{
private string _myText;
public string MyText
{
get{ return _myText; }
set
{
_myText = value;
OnPropertyChanged("MyText");
}
}
}
I would like to be able to databind the TextBox to the MyText property so that whenever I modify it in the code behind it will get updated on the page.
Converting my comment into an answer:
remove the ElementName and try RelativeSource={RelativeSource FindAncestor, AncestorType=Page}

Categories

Resources