WPF Custom Control Picking Up Parent Window's DataContext - c#

I have a ViewModel
public class ViewModel:ViewModelObject
{
public ViewModel()
{
ProjectionDataElementList = new ObservableCollection<ProjectionDataElement>();
}
public ObservableCollection<ProjectionDataElement> ProjectionDataElementList { get; set; }
private ProjectionDataElement _currentSelectedProjectionDataElement;
public ProjectionDataElement CurrentSelectedProjectionDataElement
{
get
{
return _currentSelectedProjectionDataElement;
}
set
{
_currentSelectedProjectionDataElement = value;
OnPropertyChanged("CurrentSelectedProjectionDataElement");
}
}
}
A control called ProjectorDisplayControl
<UserControl x:Class="Fast_Project.ProjectorDisplayControl"
..................>
<Viewbox>
<StackPanel>
<TextBlock Text="{Binding Path=TextBody}"/>
</StackPanel>
</Viewbox>
public partial class ProjectorDisplayControl : UserControl
{
public ProjectorDisplayControl()
{
InitializeComponent();
}
public static readonly DependencyProperty ProjectedDataProperty = DependencyProperty.Register("ProjectedData", typeof(ProjectionDataElement), typeof(ProjectorDisplayControl),
new PropertyMetadata(new PropertyChangedCallback((objectInstance, arguments) =>
{
ProjectorDisplayControl projectorDisplayControl = (ProjectorDisplayControl)objectInstance;
projectorDisplayControl._projectedData = (ProjectionDataElement)arguments.NewValue;
})));
public ProjectionDataElement ProjectedData
{
get
{
return (ProjectionDataElement)GetValue(ProjectedDataProperty);
}
set
{
SetValue(ProjectedDataProperty, value);
}
}
private ProjectionDataElement _projectedData
{
get
{
return this.DataContext as ProjectionDataElement;
}
set
{
this.DataContext = value;
}
}
}
That control is used in two places.
First place is in a ListBox where it works great:
<ListBox Grid.Column="0" ItemsSource="{Binding Path=ProjectionDataElementList}" Name="projectedDataListBox"
SelectedItem="{Binding Path=CurrentSelectedProjectionDataElement, UpdateSourceTrigger=PropertyChanged}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Border BorderThickness="1" BorderBrush="Black">
<controls:ProjectorDisplayControl x:Name="_projectorDisplay" ProjectedData="{Binding}"/>
</Border>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Second place is on top of a form where I set the form's DataContext with a ViewModel object. To make the ProjectorDisplayControl consume the CurrentSelectedProjectionDataElement in the ViewModel I would expect to have to do this:
<Window x:Class="Fast_Project.DisplayWindow"
................>
<Viewbox>
<StackPanel>
<controls:ProjectorDisplayControl x:Name="_projectorDisplay" ProjectedData="{Binding CurrentSelectedProjectionDataElement}"/>
</StackPanel>
</Viewbox>
That code gives me two binding errors:
System.Windows.Data Error: 40 :
BindingExpression path error:
'TextBody' property not found on 'object' ''ViewModel' (HashCode=2512406)'.
BindingExpression:Path=TextBody;
DataItem='ViewModel' (HashCode=2512406);
target element is 'TextBlock' (Name='');
target property is 'Text' (type 'String')
System.Windows.Data Error: 40 :
BindingExpression path error:
'CurrentSelectedProjectionDataElement' property not found on 'object' ''ProjectionDataElement' (HashCode=37561097)'.
BindingExpression:Path=CurrentSelectedProjectionDataElement;
DataItem='ProjectionDataElement' (HashCode=37561097);
target element is 'ProjectorDisplayControl' (Name='_projectorDisplay');
target property is 'ProjectedData' (type 'ProjectionDataElement')
When I watch the setter of the private property _projectedData on ProjectorDisplayControl which sets the DataContext I first see it get set with a valid value and then set to null.
In the DisplayWindow that holds the single ProjectorDisplayControl I can remove the binding to the CurrentSelectedProjectionDataElement and then I only get the first binding error message:
<Viewbox>
<StackPanel>
<controls:ProjectorDisplayControl x:Name="_projectorDisplay" />
</StackPanel>
</Viewbox>
The first binding error makes me feel like the ProjectorDisplayControl's DataContext is getting set with a ViewModel object when the DisplayWindow's DataContext is getting set. But from what I've read controls don't share the same data context with their parent window unless you set it so.
I've treating the binding path for the ProjectorDisplayControl.ProjectedData in DisplayWindow like it was a ProjectionDataElement object as the second error message states.
<Viewbox>
<StackPanel>
<controls:ProjectorDisplayControl x:Name="_projectorDisplay" ProjectedData="{Binding }"/>
</StackPanel>
</Viewbox>
Then is tells me:
Cannot create default converter to perform 'one-way' conversions between types 'Fast_Project.ViewModel' and 'Fast_Project.ProjectionDataElement'
Like it really was the ViewModel object like I thought it was in the first place...
Anyways I suspect that the root of my problem lies in the how I see the ProjectorDisplayControl having a DataContext set to the ViewModel object. Anyone see where I messed up?
Thank you all for your help!
The solution was:
ProjectorDisplayControl
<StackPanel>
<TextBlock Text="{Binding Path=ProjectedData.TextBody, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type controls:ProjectorDisplayControl}}}"/>
</StackPanel>
DisplayWindow
<StackPanel>
<controls:ProjectorDisplayControl x:Name="_projectorDisplay" ProjectedData="{Binding Path=ViewModel.CurrentSelectedProjectionDataElement,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type controls:DisplayWindow}}}"/>
</StackPanel>

Child control inherits the Dependency property value (or DataContext in this case) from their Parents.
When you are using the UserControl inside the itemTempalte, then the DataContext of each item is already ProjectionDataElement and hence the DataContext of your control is set to ProjectionDataElement.
When you are using the control inside parent it will inherit its DataContext.
The problem is that you are setting your ProjectedData property on control and not using it inside it. If you want that each of your control should be bind to the value set on ProjectedData then you should update the bindings like:
<UserControl x:Class="Fast_Project.ProjectorDisplayControl"
..................>
<Viewbox>
<StackPanel>
<TextBlock Text="{Binding Path=ProjectedData.TextBody, RelativeSource={RelativeSource Self}"/>
</StackPanel>
For you second error, you must be setting the DataContext of that window to ProjectionDataElement somewhere that is why is searching it inside it.

Related

Dependency property nerver set when binding with a custom object [duplicate]

I created a small File Browser Control:
<UserControl x:Class="Test.UserControls.FileBrowserControl"
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"
d:DesignHeight="44" d:DesignWidth="461" Name="Control">
<Grid Margin="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Margin="3" Text="{Binding SelectedFile}" IsReadOnly="True" TextWrapping="Wrap" />
<Button HorizontalAlignment="Right" Margin="3" Width="100" Content="Browse" Grid.Column="1" Command="{Binding BrowseCommand}" />
</Grid>
</UserControl>
With the following code behind:
public partial class FileBrowserControl : UserControl
{
public ICommand BrowseCommand { get; set; }
//The dependency property
public static DependencyProperty SelectedFileProperty = DependencyProperty.Register("SelectedFile",
typeof(string),typeof(FileBrowserControl), new PropertyMetadata(String.Empty));
public string SelectedFile { get{ return (string)GetValue(SelectedFileProperty);} set{ SetValue(SelectedFileProperty, value);}}
//For my first test, this is a static string
public string Filter { get; set; }
public FileBrowserControl()
{
InitializeComponent();
BrowseCommand = new RelayCommand(Browse);
Control.DataContext = this;
}
private void Browse()
{
SaveFileDialog dialog = new SaveFileDialog();
if (Filter != null)
{
dialog.Filter = Filter;
}
if (dialog.ShowDialog() == true)
{
SelectedFile = dialog.FileName;
}
}
}
And I use it like this:
<userControls:FileBrowserControl SelectedFile="{Binding SelectedFile}" Filter="XSLT File (*.xsl)|*.xsl|All Files (*.*)|*.*"/>
(SelectedFile is Property of the ViewModel of the usercontrol using this control)
Currently the issue is that when I click on Browse, the textbox in the usercontrol is correctly updating, but the SelectedFile property of the viewmodel parent control is not set(no call to the set property).
If I set the Mode of the binding to TwoWay, I got this exception:
An unhandled exception of type 'System.StackOverflowException' occurred in Unknown Module.
So what did I do wrong?
The problem is that you set your UserControl's DataContext to itself in its constructor:
DataContext = this;
You should not do that, because it breaks any DataContext based Bindings, i.e. to a view model instance that is provided by property value inheritance of the DataContext property
Instead you would change the binding in the UserControl's XAML like this:
<TextBox Text="{Binding SelectedFile,
RelativeSource={RelativeSource AncestorType=UserControl}}" />
Now, when you use your UserControl and write a binding like
<userControls:FileBrowserControl SelectedFile="{Binding SelectedFile}" />
the SelectedFile property gets bound to a SelectedFile property in your view model, which should be in the DataContext inherited from a parent control.
Do not ever set DataContext of UserControl inside usercontrol:
THIS IS WRONG:
this.DataContext = someDataContext;
because if somebody will use your usercontrol, its common practice to set its datacontext and it is in conflict with what you have set previously
<my:SomeUserControls DataContext="{Binding SomeDataContext}" />
Which one will be used? Well, it depends...
The same applies to Name property. you should not set name to UserControl like this:
<UserControl x:Class="WpfApplication1.SomeUserControl" Name="MyUserControl1" />
because it is in conflict with
<my:SomeUserControls Name="SomeOtherName" />
SOLUTION:
In your control, just use RelativeSource Mode=FindAncestor:
<TextBox Text="{Binding SelectedFile, RelativeSource={RelativeSource AncestorType="userControls:FileBrowserControl"}" />
To your question on how are all those third party controls done: They use TemplateBinding. But TemplateBinding can be used only in ControlTemplate. http://www.codeproject.com/Tips/599954/WPF-TemplateBinding-with-ControlTemplate
In usercontrol the xaml represents Content of UserControl, not ControlTemplate/
Using this:
<userControls:FileBrowserControl SelectedFile="{Binding SelectedFile}" ...
The FileBrowserControl's DataContext has already been set to itself, therefore you are effectively asking to bind to the SelectedFile where the DataContext is the FileBrowserControl, not the parent ViewModel.
Give your View a name and use an ElementName binding instead.
SelectedFile="{Binding DataContext.SelectedFile, ElementName=element}"

WPF binding does not work for an ItemsControl

I am trying to bind a dependency property to another property but it does not seem to work. I have an ItemsControl within another ItemsControl as such:
<!--This is an itemscontrol in BigBox.xaml that contains bins-->
<ItemsControl
x:Name="ctrlBin"
Grid.Column="1"
Grid.Row="1"
ItemsPanel="{StaticResource HorizontalStackPanel}"
ItemsSource="{Binding Bins}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl
ItemsSource="{Binding}"
ItemContainerStyle="{StaticResource BinViewContainer}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:BinView
x:Name="ctrlBin"
BorderBrush="Black"
BorderThickness="1"
Bin="{Binding}"
BinFlashStart="{Binding DashboardFlashStart}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This is in my 'BigBox'
private bool? m_DashboardFlashStart;
public bool? DashboardFlashStart
{
get=> m_DashboardFlashStart;
set => Set(ref m_DashboardFlashStart, value);
}
And this is in my BinView
//This is in the BinView.xaml.cs
public static DependencyProperty BinFlashStartProperty =
DependencyProperty.Register(
"BinFlashStart",
typeof(bool?),
typeof(BinView),
new PropertyMetadata(null, OnBinFlashStartSet));
private static void OnBinFlashStartSet(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
BinVM Bin = ((BinView)sender).m_BinVM;
Bin.CurBin.FlashBin = (bool?)e.NewValue;
}
public bool? BinFlashStart
{
get => (bool?) GetValue(BinFlashStartProperty);
set => SetValue(BinFlashStartProperty, value);
}
With that i get an error
System.Windows.Data Error: 40 : BindingExpression path error: 'DashboardFlashStart' property not found on 'object' ''Bin' (HashCode=-125066214)'. BindingExpression:Path=DashboardFlashStart; DataItem='Bin' (HashCode=-125066214); target element is 'BinView' (Name=''); target property is 'BinFlashStart' (type 'Nullable`1')
Why is it looking for DashboardFlashStart property on 'Bin'. I thought that is the source which comes from the BigBox.
Like if i put a static value of "True" in BigBox.xaml for BinFlashStart instead of binding then that works. Why am i not able to bind to the DashboardFlashStart of BigBox.
Please if somebody can explain what is going on, that would be really helpful.
I am new to this WPF.
This should work provided that the DashboardFlashStart property is defined in the code-behind of the BigBox class, i.e. in BigBox.xaml.cs:
BinFlashStart="{Binding DashboardFlashStart, RelativeSource={RelativeSource AncestorType=local:BigBox}}"/>
Your control is looking for a property named DashboardFlashStart in it's datacontext. But the property isn't in the "Bin" class but in the BigBox, so what you have to do is to specify for this binding the source of the binding to the bigbox datacontext.
Change your Binding BinFlashStart="{Binding DashboardFlashStart}"
With something like this
{Binding DataContext.DashboardFlashStart, ElementName=ctrlBin}"

Binding a property defined in code-behind, and another one defined in a class in the same template

I would like to binding a property defined in code-behind, and another one defined in a class in the same template with datatype.
Here's an example:
My class:
public class MyClass
{
public string name { get; set; }
public MyClass(string name)
{
this.name=name;
}
}
Code behind:
public string name2;
public MyView()
{
this.InitializeComponent();
name2 = "Tim";
}
<DataTemplate x:Key="MasterListViewItemTemplate" x:DataType="model:MyClass">
<StackPanel>
<TextBlock Text="{x:Bind name}"/>
<TextBlock Text="{x:Bind name2}"/>
</StackPanel>
</DataTemplate>
In this case obviously the first TextBlock has no problems.
I wish the second TextBlock refers to the code-behind and not in MyClass.
How can I do ?
You should set the second TextBlock's datacontext to be the current window.
I think this can be achieved by using binding expression like this
<TextBlock DataContext="{Binding ElementName=MyView, Path=.}" Text="{x:Bind name2}" />
where MyView in the binding expression is the x:Name property of the MyView window.
EDIT(WPF): This binding should work even for ResourceDictionary entries
<TextBlock DataContext="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=.}" Text="{Binding name2}" />
And something important, i see that your example, name2 is just defined in the constructor of the window. The right way to do it should look like this.
public string name2 { get; set; }
public MyView()
{
this.InitializeComponent();
this.name2 = "Tim";
}
I hope this helps.
Try the following:
In constructor of your CodeBehind:
public MyView()
{
this.InitializeComponent();
this.DataContext = this; //set the datacontext here
name2 = "Tim";
}
In your XAML:
<TextBlock Text="{Binding DataContext.name2, ElementName=MyView}"/>
First your binding always looks at the DataContext of itself first and if none is specified is traverses up the tree to owner by owner until a DataContext is assigned. It then looks there for the property to bind to. Since you have put the same property name in both textblocks and haven't specified any other means of binding they are both looking for the property in the same DataContext. In other words they are both looking at your MyClass object.
To make this work you will need to tell the Binding where to look for the property by specifying the binding more discretely.
<TextBlock Text="{Binding Name2, Mode=OneWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" />
This is assuming your DataTemplate is used on an object that is in a MainWindow type. Play with it to get yours.
Also, you will need to change the property in the code behind to be a DependencyProperty (since it's a UIElement this is easiest.)
public string Name2
{
get { return (string)GetValue(Name2Property); }
set { SetValue(Name2Property, value); }
}
public static readonly DependencyProperty Name2Property =
DependencyProperty.Register(nameof(Name2), typeof(string), typeof(MainWindow));
If you do this your DataTemplate will bind to that value.
This is just to answer the question and help you understand it some but not how I would personally design a DataTemplate.

Nested Binding With WPF

I am newbie to WPF and curious to know how to nested bind the properties in WPF. I have made a sample app for better understanding. Please find below the following scenario's.
Working Code:
UserControl1 >> MainWindow
Created a UserControl and included it in the mainWindow.
UserControl1.xaml
<StackPanel>
<TextBox x:Name="text1" Text="{Binding MyTextUC1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox />
</StackPanel>
UserControl1.xaml.cs
public UserControl1()
{
InitializeComponent();
this.DataContext = this;
}
public static readonly DependencyProperty MyTextUC1Property =
DependencyProperty.Register("MyTextUC1", typeof(string), typeof(UserControl1));
public string MyTextUC1
{
get { return (string)GetValue(MyTextUC1Property); }
set { SetValue(MyTextUC1Property, value); }
}
MainWindwo.xaml
<StackPanel>
<local:UserControl1 x:Name="text1"/>
<TextBlock Text="{Binding ElementName=text1, Path=MyTextUC1}" />
</StackPanel>
On running this, text changes in the TextBox of UserControl1 got reflected in TextBlock of MainWindow.
Not Working:
UserControl1 >> UserControl2 >> MainWindow
Created a UserControl(usercontrol1) and included it in another UserControl(usercontrol2) and then included the UserControl2 in mainWindow.
UserControl1.xaml & UserControl1.xaml.cs
//Same as Working Code
UserControl2.xaml
<Grid>
<local:UserControl1 x:Name="text2" MyTextUC1="{Binding MyTextUC2,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
UserControl2.xaml.cs
public UserControl2()
{
InitializeComponent();
this.DataContext = this;
}
public static readonly DependencyProperty MyTextUC2Property =
DependencyProperty.Register("MyTextUC2", typeof(string), typeof(UserControl2));
public string MyTextUC2
{
get { return (string)GetValue(MyTextUC2Property); }
set { SetValue(MyTextUC2Property, value); }
}
MainWindwo.xaml
<StackPanel>
<local:UserControl2 x:Name="text1"/> //UserControl2 contains UserControl1
<TextBlock Text="{Binding ElementName=text1, Path=MyTextUC2}" />
</StackPanel>
On Running this, text changes in the TextBox of UserControl2 NOT REFLECTED in TextBlock of MainWindow.
I'm pretty sure that this is a basic thing in WPF and I may be implementing this in wrong way. Can someone suggest me where I am wrong? Thanks in advance.
If you check the Debug Output you will see the following error:
System.Windows.Data Error: 40 : BindingExpression path error: 'MyTextUC2' property not found on 'object' ''UserControl1' (Name='text2')'. BindingExpression:Path=MyTextUC2; DataItem='UserControl1' (Name='text2'); target element is 'UserControl1' (Name='text2'); target property is 'MyTextUC1' (type 'String')
The problem is with this binding:
<local:UserControl1 x:Name="text2" MyTextUC1="{Binding MyTextUC2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
It is looking for the MyTextUC2 property in UserControl1. It is because you set the DataContext in UserControl1's constructor to this (UserControl1). So every binding on UserControl1 will be evaluated against UserControl1's DataContext (which is itself).
If you change it to this:
<local:UserControl1 x:Name="text2"
MyTextUC1="{Binding MyTextUC2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type local:UserControl2}}}" />
it should start working.

How to make a Binding when DataContext is already set

I have a custom UserControl of which I am setting the DataContext as a binding to some object. I also want to enable or disable the control based on a binding to a boolean in the parent object. This however fails, cause as soon as the data context is set, the system tries to find all other bindings in the new data context instead of the old one. (This seems a bit weird to me anyway.)
public class Animal
{
public string Name;
}
public class Zoo
{
public Zoo ()
{
AnimalOnDisplay = new AnimalOnDisplay { Name = "Tyrannosaurus" };
}
public bool ZooIsClosed;
public Animal AnimalOnDisplay;
}
static void Main()
{
ZooUserControl control = new ZooUserControl ();
control.DataContext = new Zoo();
control.Show();
}
XAML:
<UserControl x:Class="MyProgramme.ZooUserControl"
xmlns:zoo="clr-namespace:Zoo.UserControls">
<StackPanel>
<Label Content="Welcome!" />
<zoo:AnimalUserControl DataContext="{Binding AnimalOnDisplay}"
IsEnabled="{Binding ZooIsClosed}" />
</StackPanel>
</UserControl>
The DataContext of the above user control is a valid instance of Zoo (I checked this). This gives the following error:
System.Windows.Data Error: 40 : BindingExpression path error: 'ZooIsClosed' property not found on 'object' ''Animal`1' (HashCode=44290843)'.
BindingExpression:Path=ZooIsClosed; DataItem='Animal`1' (HashCode=44290843); target element is 'AnimalUserControl' (Name='');
target property is 'IsEnabled' (type 'Boolean')
So clearly, it's looking for ZooIsClosed in the wrong place. I tried to bind it to the current DataContext like this:
IsEnabled="{Binding ZooIsClosed, RelativeSource={RelativeSource Self}}"
which produces the same error, and with an ElementName, which didn't work either.
How can I bind it to the correct variable (i.e. the ZooIsClosed in Zoo)?
You can set the Binding for the IsEnabled by using RelativeSource tracking up to the UserControl like this:
<zoo:AnimalUserControl DataContext="{Binding AnimalOnDisplay}"
IsEnabled="{Binding DataContext.ZooIsClosed,
RelativeSource={RelativeSource AncestorType=UserControl}}"/>
Note the Path is set to DataContext.ZooIsClosed.
Also your model is not properly written (I hope it's just demonstrative).
What you have done is asked the binding to search for a property called ZooIsClosed on your AnimalOnDisplay property. We can see from your code behind that this relationship does not exist.
Because both ZooIsClosed and AnimalOnDisplay are properties of your Zoo class, so what you should do is set your DataContext to your Zoo class instance (assuming your ZooControl has a Zoo instance DependencyProperty), then bind to properties on that instance, i.e. IsZooClosed and AnimalOnDisplay.
Here is a bit of code to get you started, see if it fits your needs:
<UserControl x:Class="MyProgramme.ZooUserControl"
xmlns:zoo="clr-namespace:Zoo.UserControls"
DataContext="{Binding Zoo.DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type zoo:ZooControl}}}">
<StackPanel>
<Label Content="Welcome!" />
<zoo:AnimalUserControl IsEnabled="{Binding ZooIsClosed}" />
</StackPanel>
</UserControl>

Categories