I have a control that has an ObservableCollection of one or more key-value pairs. That can be provided as a ready-made ObservableCollection by the ViewModel bound as usual (Keys={Binding KeyCollection} -- which works perfectly), but I'd like to be able to define it in XAML as well:
<foo:KeyControl>
<foo:KeyItem Key="ID" Value="{Binding ID}" />
<foo:KeyItem Key="HatSize" Value="{Binding HatSize}" />
</foo:KeyControl>
KeyItem is derived from FrameworkElement, and the properties Key and Value are dependency properties. I've got a ContentPropertyAttribute on KeyControl, and that's working fine: The correct collection property is populated, and the Key properties (the ones with literal values, not bindings) are initialized as in the XAML.
The trouble is that the bindings for the Value properties don't work. They always assign null to the property. I think that's because the KeyItem instances have a null DataContext.
Also, RelativeSource FindAncestor thinks there aren't any ancestors to find:
<foo:KeyItem Type="ID"
Value="{Binding Path=DataContext.ID,
RelativeSource={RelativeSource FindAncestor, AncestorType=foo:MyView},
diag:PresentationTraceSources.TraceLevel=High}" />
When new KeyItem instances are added to the ObservableCollection, I've tried setting their DataContext to be that of the control, but the control's DataContext is always null at that point (?!) if they're defined in XAML.
What am I missing?
UPDATE
The content of the answer was in a linked article by Thomas Levesque, so in case that goes offline, here's the fix: You create a proxy as a resource. Where you define the resource, the control's DataContext is in scope. In the bindings on the collection item properties, you can get to the resource.
C#:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables
// animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object),
typeof(BindingProxy), new UIPropertyMetadata(null));
}
XAML:
<foo:KeyControl>
<foo:KeyControl.Resources>
<foo:BindingProxy x:Key="proxy" Data="{Binding}" />
</foo:KeyControl.Resources>
<foo:KeyItem Key="ID" Value="{Binding Data.ID,
Source={StaticResource proxy}}" />
<foo:KeyItem Key="HatSize" Value="{Binding Data.HatSize,
Source={StaticResource proxy}}" />
</foo:KeyControl>
Bit of a kludge, but it works. I think I may just stick with binding the collection from the ViewModels, though.
For search purposes, I was getting the "Framework mentor not found" error on this when the DataContexts were null.
I had a similar problem with the datacontext not inheriting and I solved my problem using the proxy technique described here
http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/
Hope this helps
Related
In a datagrid I need a column whose cells are sometimes readonly
I use the bindingproxy method:
<local:MyDataGrid.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</local:MyDataGrid.Resources>
where Data is a custom DependencyProperty
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy),
new UIPropertyMetadata(null));
In xaml I have:
<DataGridCheckBoxColumn Header="MyProp"
IsReadOnly="{Binding MyProp.IsReadOnly, Source={StaticResource proxy}"
Binding="{Binding MyProp.MyValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Visibility="{Binding Data.AnotherProp, Source={StaticResource proxy}, Converter={StaticResource BoolToVisibilityConverter}}"
The binding for Visibility and Binding works as expected.
Data is the viewmodel's DataContext and AnotherProp is one of its properties.
The viewmodel also has the property public ObservableCollection<MyItem> MyItems { get; private set; }
which is of course the ItemsSource of the DataGrid and indeed Binding is bound to the properties of each single MyItem of each row.
This doesn't work for the IsReadOnly binding though. What should I do to make it work?
BindingExpression path error: 'MyProp' property not found on object BindingProxy.
This is understandable because MyProp does not belong to Data.
But why does the binding of Binding work?
Also IsReadOnly="{Binding Data.MyProp.IsReadOnly, Source={StaticResource proxy}}" of course doesn't work because MyProp is a property of MyItem and not of the viewmodel.
Also IsReadOnly="{Binding MyProp.IsReadOnly}" without the bindingproxy doesn't work.
Cannot find governing FrameworkElement or FrameworkContentElement for target element.
The Binding property is special. It's type is BindingBase and it's used to apply a binding to the element that gets created in the GenerateElement method, i.e. you don't really bind this property to a source property. You rather set it to a Binding object.
If you want to bind to IsReadOnly, you need to use a proxy like you do when binding to the Visibility property.
I'm building a control that can edit POCOs. There is a descriptor collection for the fields within the POCO that need to be edited and I'm binding a ListBox's ItemsSource to this collection. Amongst other things, the descriptor gives me the ability to select a suitable DataTemplate and the variable name in the POCO that this ListBox item should edit.
My ListBox is built like this:
<ListBox ItemsSource="{Binding ColumnCollection, ElementName=root}">
<ListBox.Resources>
<DataTemplate x:Key="TextTemplate">
<StackPanel>
<TextBlock Text="{Binding DisplayName}" />
<!-- !!! Question about following line !!! -->
<TextBox Text="{Binding ElementName=vm.CurentEditing, Path=PathName}" />
</StackPanel>
</DataTemplate>
<!-- Details omitted for brevity -->
<DataTemplate x:Key="PickListTemplate" />
<DataTemplate x:Key="BooleanTemplate" />
</ListBox.Resources>
<ListBox.ItemTemplateSelector>
<local:DataTypeSelector
TextTemplate="{StaticResource TextTemplate}"
PickListTemplate="{StaticResource PickListTemplate}"
BooleanTemplate="{StaticResource BooleanTemplate}"
/>
</ListBox.ItemTemplateSelector>
</ListBox>
It is the TextBox binding expression in the "TextTemplate" that I am having problems with. The problem is that "PathName" should not be taken as a literal string, but is the name of a string property in the ColumnDescription class (the collection type of ColumnCollection used for ListBox.ItemsSource), which gives the name of the POCO property I want to bind to (the POCO is "vm.CurrentEditing").
Is there some way to use the value of a property in XAML as input to a binding expression, or will I have to resort to code behind?
(Incidentally, specifying the ElementName as "x.y" as I have done above also seems to be invalid. I assume the "y" part should be in Path but that's currently taken up with my property name...!)
So you want to bind TextBox.Text to Property X of Object Y, where X and Y both change at runtime.
It sounds like what you want to do is something analogous to ListBox.DisplayMemberPath: You can bind a string or PropertyPath property to DisplayMemberPath and it'll work. The way I've done stuff like that is to have a dependency property of type String or PropertyPath, and programatically create a binding from that to whatever property.
So, I wrote an attached property which creates a binding.
public class POCOWrangler
{
#region POCOWrangler.BindPropertyToText Attached Property
public static String GetBindPropertyToText(TextBox obj)
{
return (String)obj.GetValue(BindPropertyToTextProperty);
}
public static void SetBindPropertyToText(TextBox obj, PropertyPath value)
{
obj.SetValue(BindPropertyToTextProperty, value);
}
public static readonly DependencyProperty BindPropertyToTextProperty =
DependencyProperty.RegisterAttached("BindPropertyToText", typeof(String), typeof(POCOWrangler),
new PropertyMetadata(null, BindPropertyToText_PropertyChanged));
private static void BindPropertyToText_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is String && d is TextBox)
{
var tb = d as TextBox;
var binding = new Binding((String)e.NewValue);
// The POCO object we're editing must be the DataContext of the TextBox,
// which is what you've got already -- but don't set Source explicitly
// here. Leave it alone and Binding.Source will be updated as
// TextBox.DataContext changes. If you set it explicitly here, it's
// carved in stone. That's especially a problem if this attached
// property gets initialized before DataContext.
//binding.Source = tb.DataContext;
binding.Mode = BindingMode.TwoWay;
BindingOperations.SetBinding(tb, TextBox.TextProperty, binding);
}
}
#endregion POCOWrangler.BindPropertyToText Attached Property
}
And I wrote a quick example thing: There's a little class named Foo that has a Name property, and a viewmodel with two properties, Foo Foo and String DisplayPathName. It works! Of course, this depends on default TextBox editing behavior for whatever type the property happens to be. I think that will get you the same results as if you'd bound explicitly in XAML, but it sitll won't always necessarily be just what you want. But you could very easily go a little nuts and add some triggers in the DataTemplate to swap in different editors, or write a DataTemplateSelector.
I stuffed ViewModel.Foo in a ContentControl just to get a DataTemplate into the act, so that the TextBox gets his DataContext in the same manner as yours.
Note also that I'm getting DisplayPathName by a relative source from something outside the DataContext object -- it's not a member of Foo, of course, it's a member of the viewmodel.
C#
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel {
DisplayPathName = "Name",
Foo = new Foo { Name = "Aloysius" }
};
}
XAML
<ContentControl
Content="{Binding Foo}"
>
<ContentControl.ContentTemplate>
<DataTemplate>
<TextBox
local:POCOWrangler.BindPropertyToText="{Binding
DataContext.DisplayPathName,
RelativeSource={RelativeSource AncestorType=ContentControl}}"
/>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
That was fun.
I am having problems with Binding. Since RelativeSource needs the visual tree to travel up and find the desired Ancestor, you are only allowed to use it on an UIElement but I am trying to do a RelativeSource binding on an Non-UIElement, such as is a ValidationRule, which as you all know isnt inside the VisualTree nor its UIElement. As you can expect the binding breaks. RelativeSource couldn't be found because like i said there is no VisualTree or LogicalTree available. I need to make it work though.
Here is an example of XAML:
<StackPanel DataContext{Binding}>
<Grid>
<ContentControl Content{Binding MVPart1>
<TextBox>
<TextBox.Text>
<Binding Path="VMPart1Property1">
<Binding.ValidationRules>
<my:MyValidationRule>
<my:ValidationRule.DOC>
<my:DepObjClass DepProp={Binding Path=DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type StackPanel}}}/>
</my:ValidationRule.DOC>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</ContentControl>
</Grid>
</StackPanel>
So basically MyValidationRule is derivering from ValidationRule class, but thats not UIElement nor DependencyObject and therefore I had to create a class which derivates from DependencyObject called DepObjClass to be able to write down the xaml binding expression.
Here is code:
public class MyValidationRule : ValidationRule
{
public DepObjClass DOC
{
get;
set;
}
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
string text = value as string;
if (!string.IsNullOrEmpty(text))
{
return new ValidationResult(true, string.Empty);
}
return new ValidationResult(false, "Not working blahhh");
}
}
public class DepObjClass : DependencyObject
{
public object DepProp
{
get
{
return (object)GetValue(DepPropProperty);
}
set
{
SetValue(DepPropProperty, value);
}
}
public static DependencyProperty DepPropProperty
= DependencyProperty.Register(typeof(object), typeof(DepObjClass)......);
}
Now to sum up. MyValidatonRule is not UIElement its not DependencyObject but it has a property of a type that is, hence why the xaml binding expression compiles.
When I run the application the binding itself isnt working because StackPanel couldnt be found because ValidationRule doesnt have VisualTree nor my validation rule participates in Logical or Visual Tree.
The question is how do I make such case work, how to find StackPanel from an Non-UIElement such as my ValidationRule?
I appologize for my code not comipiling but I hope you can understand what I am trying to do.
I am giving 50 points to you guys for the right answer.
You can do the following:
Create a helper component which derives from Freezable and defines a DependencyProperty for what you want to bind.
Create a ValidationRule with a property which takes an object of the helper component, similar to what you have done already.
Declare an instance of the helper component in the Resources of an object which can bind to whatever you want to bind. Freezable and its derived classes inherit the binding context (the location in the logical tree) of any control in whose Resources they are declared, so there you can create your binding.
When declaring the ValidationRule, use {StaticResource} to assign the helper component to the property in the ValidationRule. StaticResource works without a binding context, as long as the resource is declared before it is used.
The XAML would look like this:
<StackPanel>
<StackPanel.Resources>
<my:Helper x:Key="helper" ValProperty="{Binding}"/>
</StackPanel.Resources>
<Grid>
<TextBox DataContext="{Binding MVPart1}">
<TextBox.Text>
<Binding Path="VMPart1Property1">
<Binding.ValidationRules>
<my:MyValidationRule Helper="{StaticResource helper}"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</Grid>
</StackPanel>
I'm trying to bind two ListBoxes:
<ListBox SelectionChanged="lbApplications_SelectionChanged"
ItemsSource="{Binding Path=Applications,
UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
<ListBox DisplayMemberPath="Message"
ItemsSource="{Binding Path=Events,
UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
Applications and Events are public properties in Window class.
I set DataContext to this to both list boxes and implement INotifyPropertyChanged in Window class:
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
And then after adding new item to Applications or Events I call:
NotifyPropertyChanged("Events");
NotifyPropertyChanged("Applications");
The issue is that ListBox is loaded only one time. What am I doing wrong?
Let's just look at one of the ListBoxes, since they're both the same, basically.
The code we're concerned about is this:
<ListBox ItemsSource="{Binding Path=Applications,
UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
Since you're new to WPF, let me say you probably don't need UpdateSourceTrigger or Mode in there, which leaves us with this:
<ListBox ItemsSource="{Binding Path=Applications}" />
You mentioned that Applications is a public property in your code-behind. You need it to be a DependencyProperty, and you need it to fire events when it changes -- most people use an ObservableCollection for this.
So your code-behind will have something like this:
public ObservableCollection<string> Applications
{
get { return (ObservableCollection<string>)GetValue(ApplicationsProperty); }
set { SetValue(ApplicationsProperty, value); }
}
public static readonly DependencyProperty ApplicationsProperty =
DependencyProperty.Register("Applications",
typeof(ObservableCollection<string>), typeof(Window1),
new UIPropertyMetadata(null));
Then, where you want to add it, you'll do something like this:
this.Applications = new ObservableCollection<string>();
Applications.Add("Whatever");
Finally, for the "simple" binding syntax to work in the XAML, I usually change the DataContext in my Window (or the root Control element for the file, whatever I'm working in) to
<Window DataContext="{Binding RelativeSource={RelativeSource Self}}" ... >
...
Your Applications box will update automatically.
The problem is that your property value hasn't changed. It's still the same list, same reference.
One solution might be that your collections are of type ObservableCollection. These lists provide events for WPF when you add or remove items.
I've created two UserControls, the first of which displays a list of objects, the second of which displays details about an object that is selected from the first. I've created a dependency property on the first control and am binding each UserControl to an object declared in my Resources collection. I've seen blog posts describing this, but cannot seem to get it to work. I am getting a XamlParseException. The funny thing is the exception only occurs when I set the binding Mode=TwoWay on my first UserControls. Here's the code...
Page.xaml
<UserControl.Resources>
<local:Item x:Key="SelectedItem" />
</UserControl.Resources>
...
<controls:ItemList
SelectedItem="{Binding Mode=TwoWay, Source={StaticResource SelectedItem}}">
</controls:ItemList >
...
<controls:ItemDetails
DataContext="{Binding Source={StaticResource SelectedItem}}">
</controls:ItemDetails>
ItemList.xaml.cs
public partial class ItemList: UserControl
{
public ItemList()
{
InitializeComponent();
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(Item), typeof(ItemList), new PropertyMetadata(new Item()));
public Item SelectedItem
{
get { return (Item )GetValue(SelectedItemProperty ); }
set { SetValue(SelectedItemProperty , value); }
}
Any suggestions are welcome!
Your Xaml is incorrect, from the looks of it. You are missing a property that you need to bind to for two-way. You are saying that you want to bind to object defined in source, but you don't specify a property of that resource to bind to. In this case, the SelectedItem resource is an object of type Item ... you need to bind to property of Item. So if item has a property named value, your Xaml could look like this:
SelectedItem="{Binding Value, Source={StaticResource SelectedItem}, Mode=TwoWay}"
Try this instead:
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
ib.