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>
Related
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}"
I want to bind the content of my TabControl to an instance of my StepViewModel in a ObservableCollection Steps.
My ProcessViewModel:
pubic class ProcessViewModel : ViewModelBase
{
public ObservableCollection<StepViewModel> Steps
{
get { return _steps; }
set { _steps = value; OnPropertyChanged("Steps"); }
}
public StepViewModel SelectedStep
{
// like above...
}
}
My StepViewModel (DataContext should be the StepVMs in Steps of ProcessVM):
public class StepViewModel : ViewModelBase
{
public string Name { get {...} set {...} }
public object Media { get {...} set {...} }
//...
}
My TabControl (DataContext is ProcessViewModel):
<C1:C1TabControl
ItemsSource="{Binding Steps}"
SelectedItem="{Binding SelectedStep}"
SelectionChanged="{tcSteps_OnSelectionChanged">
<C1:C1TabControl.ContentTemplate>
<DataTemplate>
<local:StepView
DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type vmns:ProcessViewModel}}, Path=SelectedStep}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
</local:StepView>
</DataTemplate>
</C1:C1TabControl.ContentTemplate>
</C1:C1TabControl>
The Compiler delivers the following output message:
"System.Windows.Data Error: 40 : BindingExpression path error:
'SelectedStep' property not found on 'object' ''StepViewModel'
(HashCode=32952144)'. BindingExpression:Path=SelectedStep;
DataItem='StepViewModel' (HashCode=32952144); target element is
'StepView' (Name='StepView'); target property is 'DataContext' (type
'Object')"
Does anyone know how I can solve the?
Thanks!
It looks like there could be a few small issues with the RelativeSource on your DataContext binding for the StepView. Since the template is not part of the visual tree, I don't think you can use FindAncestor. You can use a StaticResource as a pointer to your main DataContext (eg http://www.codeproject.com/Articles/27432/Artificial-Inheritance-Contexts-in-WPF), but I think it might be simpler to just search by ElementName instead in this case. That method would look something like this:
Update your TabControl to have a name, so it is searchable in bindings by ElementName
<C1:C1TabControl
x:Name="MyTabControl"
ItemsSource="{Binding Steps}"
SelectedItem="{Binding SelectedStep}"
SelectionChanged="{tcSteps_OnSelectionChanged">
Update your StepView to look for the TabControl's DataContext by ElementName
<local:StepView DataContext="{Binding ElementName=MyTabControl, Path=DataContext.SelectedStep}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
</local:StepView>
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.
I'm trying to create a simple Bindable property called MyBoolValue in my UserControl class
First, here the xaml
<UserControl x:Class="TMDE.Controls.SimNaoRadioPicker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="16"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" Content="Teste" IsChecked="{Binding Path=MyBoolValue}" x:Name="chk" />
</Grid>
</UserControl>
And here the code-behind:
public partial class SimNaoRadioPicker : UserControl
{
public SimNaoRadioPicker()
{
InitializeComponent();
}
public bool? MyBoolValue
{
get
{
return (bool?)GetValue(MyCustomPropertyProperty);
}
set
{
SetValue(MyCustomPropertyProperty, value);
}
}
// Using a DependencyProperty as the backing store for MyCustomProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyCustomPropertyProperty =
DependencyProperty.Register("MyBoolValue",
typeof(bool?), typeof(SimNaoRadioPicker),
new UIPropertyMetadata(MyPropertyChangedHandler));
public static void MyPropertyChangedHandler(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
// Get instance of current control from sender
// and property value from e.NewValue
// Set public property on TaregtCatalogControl, e.g.
((SimNaoRadioPicker)sender).chk.IsChecked = (bool?)e.NewValue;
}
}
Now, when a try to use this control in another Window, like this:
<my:SimNaoRadioPicker x:Name="test" MyBoolValue="{Binding QCV_Localizacao_Reutilizacao}" Height="16" HorizontalAlignment="Left" Margin="287,456,0,0" VerticalAlignment="Top" Width="167" />
the Binding doesnt working, the property QCV_Localizacao_Reutilizacao doesnt get update and vice-versa.
The DataContext of the Window its a class that implements INotifyPropertyChanged, so the
property "QCV_Localizacao_Reutilizacao" should work ok.
Also if I use a regular CheckBox instead of my UserControl, its works okay
What I'm doing wrong?
I would remove the nullable part of the boolean and just make it a boolean, then set binding modes to two way.
There are two major issues -
First, your binding mode needs to be TwoWay which you can achieve in two ways -
Either specifed it to be TwoWay in xaml like this -
<my:SimNaoRadioPicker MyBoolValue="{Binding QCV_Localizacao_Reutilizacao,
Mode=TwoWay}"/>
The drawback with above apporach is that you have to explicitly set the mode whenever you are using the UserControl's instance.
Another approach would be to modify your DP itself to say that it always be bind by default in a TwoWay mode like this using FrameworkPropertyMetadata -
public static readonly DependencyProperty MyCustomPropertyProperty =
DependencyProperty.Register("MyBoolValue",
typeof(bool?), typeof(SimNaoRadioPicker),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
MyPropertyChangedHandler));
Secondly, QCV_Localizacao_Reutilizacao property lies in your Window's DataContext. But, by default any control will look for binding in its own dataContext so you explicilty need to tell it to look into Window's DataContext using RelativeSource like this -
<my:SimNaoRadioPicker MyBoolValue="{Binding QCV_Localizacao_Reutilizacao,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=Window}/>
In my application I'd like to have a drop-down box to choose a table to edit (of about 20). Each table should be represented by its own WPF DataGrid. (I'd thought about using a single DataGrid and creating a set of new columns at runtime with code-behind, but that doesn't seem very XAML-ish.)
My drop-down is in a UserControl (since it's part of a larger application). I believe (from my research) that the placeholder for the one-of-20 DataGrids should be a ContentControl used as a placeholder here:
<UserControl x:Class="MyClass" ...
xmlns:my="clr-namespace:MyNamespace"
DataContext="{Binding ViewModel}">
<StackPanel>
<Grid>
<ComboBox Name="DataPaneComboBox" HorizontalAlignment="Stretch"
IsReadOnly="True" MinWidth="120"
Focusable="False" SelectedIndex="0"
DockPanel.Dock="Left" Grid.Column="0"
SelectionChanged="DataPaneComboBox_SelectionChanged">
<ComboBoxItem Name="FirstOption" Content="Choice 1" />
<ComboBoxItem Name="SecondOption" Content="Choice 2" />
<ComboBoxItem Name="ThirdOption" Content="Choice 3" />
</ComboBox>
</Grid>
<ContentControl Name="DataGridView" Margin="0,3,0,3" Content="{Binding CurrentView}" />
</StackPanel>
Here's my code-behind for this class:
public partial class MyClass : UserControl {
private MyViewModel ViewModel {
get; set;
}
public MyClass() {
InitializeComponent();
ViewModel = new MyViewModel();
ViewModel.CurrentView = new DataGridChoice1();
}
}
And the ViewModel (class ObservableObject implements the INotifyPropertyChanged interface):
public class MyViewModel : ObservableObject {
private UserControl _currentView;
public UserControl CurrentView {
get {
if (this._currentView == null) {
this._currentView = new DatGridChoice1();
}
return this._currentView;
}
set {
this._currentView = value;
RaisePropertyChanged("CurrentView");
}
}
#endregion
}
And one of the 20 or so UserControls that can be substituted in at runtime:
<UserControl x:Class="Choice1Control"
xmlns:my="clr-namespace:MyNamespace">
<DataGrid ItemsSource="{Binding Choice1Objects}" />
<!-- ... -->
</DataGrid>
</UserControl>
When the user changes the drop-down I'd like the program to load the appropriate DataGrid. Right now I can't see the child UserControl (here, Choice1Control). I've added the child directly (without the intervening ContentControl) and it works fine.
I've tried just about every combination of DataContext and UserControl content binding. I'm new to WPF, so I'm probably missing something glaringly obvious. Thanks!
Path needs a Source to go against (Source, DataContext, RelativeSource, ElementName). ElementName can only be used to refer to elements declared in XAML by their x:Name.
For some reason it never occurred to me that there would be Binding errors written clearly in the log at run time. I futzed around until I actually got a useful message and could get to the root of the problem.
It seems that the DataContext of the root UserControl was getting intercepted before it could be inherited by the ContentControl. (Or, my impression of how DataContext is inherited/propagated is wrong.)
In the end I changed the MyClass constructor to explicitly specify the DataContext as the ViewModel.
public MyClass() {
InitializeComponent();
ViewModel = new MyViewModel();
ViewModel.CurrentView = new DataGridChoice1();
this.DataContext = ViewModel; // <-- Added this line
}
Bindings then work as expected, and I'm able to change between multiple DataGrids as the drop-down box changes state.
I'd love to hear why the initial binding was wrong. However, I'll claim a small victory now and leave that conundrum for another day.