WPF Binding itemscontrol child controls - c#

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}

Related

BindingExpression path error when attempting to bind to a C# class field

I have a fairly straightforward binding (with {Binding}, not {x:Bind}) that is not working:
<ListView
ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Item">
<StackPanel>
<TextBlock Text="{Binding MyField}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
It compiles, but fails with the error message:
Error: BindingExpression path error: 'MyField' property not found on 'MyNamespace.Item'. BindingExpression: Path='MyField' DataItem='MyNamespace.Item'; target element is 'Windows.UI.Xaml.Controls.TextBlock' (Name='null'); target property is 'Text' (type 'String')
The same binding works with x:Bind:
<TextBlock Text="{x:Bind MyField}" />
ViewModel and Item definitions
namespace MyNamespace
{
public class Item : INotifyPropertyChanged
{
public Item()
{
// ...
}
public readonly string MyField = "Foo";
// ...
}
public class MainPageViewModel : INotifyPropertyChanged
{
public MainPageViewModel()
{
// ...
}
private ItemsCollection _itemsCollection = new ItemsCollection();
public ObservableCollection<Item> Items
{
get { return _itemsCollection.Items; }
}
// ...
}
}
Disclaimer: I work for Microsoft.
MyField is a C# class field, which cannot be bound to with {Binding}.
To avoid this, change MyField to a property:
public class Item : INotifyPropertyChanged
{
// ...
private readonly string m_myField = "Foo";
public string MyField { get => m_myField; }
}
Then {Binding MyField} will work properly.
See this StackOverflow question about the difference between C# fields and properties:
4. Only Properties can be used in Binding Source
Binding Source helps us to decrease the number of lines of code. Fields are not accepted by BindingSource. We should use Properties for that.
This seems to be similar to WPF (which also can't bind on fields).

WPF change value of a child inside UserControl

I need to change a value from MainWindow of a Control inside my CustomControl.
So lets say I want to change the Labels Content inside UserControl MyControl from MainWindow.xaml.
Example:
<UserControl x:Class="XXXXX.MyUserControl"
.
.
.
>
<Grid>
<Label x:Name="TestLabel"/>
</Grid>
</UserControl>
And in MainWindow.xaml:
<MyUserControl x:Name="TestControl" />
Now how can I access Label.Content from Xaml Designer in MainWindow.xaml?
I didn't find anything out there, so hopefully someone knows how to do that.
Thanks a lot
Expose a custom Property in your UserControl, like below
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
var dpd = DependencyPropertyDescriptor.FromProperty(LabelContentProperty, typeof(MyUserControl));
dpd.AddValueChanged(this, (sender, args) =>
{
_label.Content = this.LabelContent;
});
}
public static readonly DependencyProperty LabelContentProperty = DependencyProperty.Register("LabelContent", typeof(string), typeof(MyUserControl));
public string LabelContent
{
get
{
return GetValue(LabelContentProperty) as string;
}
set
{
SetValue(LabelContentProperty, value);
}
}
}
In xaml of MainWindow
<MyUserControl x:Name="TestControl" LabelContent="Some Content"/>
Added the Following to your UserControl
<UserControl x:Class="XXXXX.MyUserControl"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
.
.
>
Have the User Control Implement INotifyPropertyChanged
Add a Property to the user control like this
Private _LabelText As String
Public Property LabelText() As String
Get
Return _LabelText
End Get
Set(ByVal value As String)
_LabelText = value
OnPropertyChanged("LabelText")
End Set
End Property
Update the Label to Bind from that Property
<Label x:Name="TestLabel" Content="{Binding Path=LabelText}"/>
Then in your MainWindow you can change the property accourdingly
<MyUserControl x:Name="TestControl" LabelText="Testing" />
Then your code behind can also reference that property

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

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

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.

WPF: Basic question about Dependency Properties

I have the following Xaml in a Window (ArtistInfo):
<Grid>
<TextBlock Text="{Binding Artist.Name}"></TextBlock>
</Grid>
And this is the code-behind for the same window (code simplified for question's sake):
public static readonly DependencyProperty ArtistProperty =
DependencyProperty.Register("Artist", typeof(Artist), typeof(ArtistInfo));
Artist Artist {
get {
return (Artist)GetValue(ArtistProperty);
}
set {
SetValue(ArtistProperty, value);
}
}
public ArtistInfo() {
InitializeComponent();
}
public ArtistInfo(int artistID) {
InitializeComponent();
Artist = GetArtist(artistID);
}
Basically what I'm trying to do is data binding to a Dependency Property, so that when Artist is populated (in the constructor), the TextBlock gets filled with the Artist's name.
What am I missing here?
The only thing I didn't see was you updating the Binding source for the TextBlock. First add a name to the TextBlock
<TextBlock Name="m_tb" ... />
Then update the DataContext value in the constructor
public ArtistInfo() {
...
m_tb.DataContext = this;
}
EDIT OP mentioned that there may be more than one TextBlock or child element.
In that case I would do the above trick for the closest parent object to all of the values. In this case the Grid control. The DataContext property will be inherited so to speak by all of the inner children.
<Window DataContext="{Binding RelativeSource={RelativeSource Self}}" ...>
<Grid>
<TextBlock Text="{Binding Artist.Name}"/>
</Grid>
</Window>

Categories

Resources