I have no idea why data binding is not happening for certain objects in my Silverlight 4 application. Here's approximately what my XAML looks like:
<sdk:DataGrid>
<u:Command.ShortcutKeys>
<u:ShortcutKeyCollection>
<u:ShortcutKey Key="Delete" Command="{Binding Path=MyViewModelProperty}"/>
</u:ShortcutKeyCollection>
</u:Command.ShortcutKeys>
</sdk:DataGrid>
The data context is set just fine since other data bindings that I have set on the grid are working just fine. The Command.ShortcutKeys is an attached DependencyProperty that is declared as follows:
public static readonly DependencyProperty ShortcutKeysProperty = DependencyProperty.RegisterAttached(
"ShortcutKeys", typeof(ShortcutKeyCollection),
typeof(Command), new PropertyMetadata(onShortcutKeysChanged));
private static void onShortcutKeysChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var shortcuts = args.NewValue as ShortcutKeyCollection;
if (obj is UIElement && shortcuts != null)
{
var element = obj as UIElement;
shortcuts.ForEach(
sk => element.KeyUp += (s, e) => sk.Command.Execute(null));
}
}
public static ShortcutKeyCollection GetShortcutKeys(
DependencyObject obj)
{
return (ShortcutKeyCollection)obj.GetValue(ShortcutKeysProperty);
}
public static void SetShortcutKeys(
DependencyObject obj, ShortcutKeyCollection keys)
{
obj.SetValue(ShortcutKeysProperty, keys);
}
I know this attached property is working just fine since the event handlers are firing. However, the Command property of the ShortcutKey objects are not getting data bound. Here's the definition of ShortcutKey:
public class ShortcutKey : DependencyObject
{
public static readonly DependencyProperty KeyProperty = DependencyProperty.Register(
"Key", typeof(Key), typeof(ShortcutKey), null);
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
"Command", typeof(ICommand), typeof(ShortcutKey), null);
public Key Key
{
get { return (Key)GetValue(KeyProperty); }
set { SetValue(KeyProperty, value); }
}
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
}
public class ShortcutKeyCollection : ObservableCollection<ShortcutKey> { }
The property that is getting bound to has its value set in the constructor of my view model, and its type is ICommand. So why isn't my Command property getting data bound? Also, have you found an effective way to debug data binding issues in Silverlight?
Edit:
At least one thing that was wrong was that ShortcutKey derived from DependencyObject instead of FrameworkElement, which is apparently the only root class that binding can be applied to. However, even after that change, the binding continued to not work properly.
You need to specify the Source of the Binding, since the DataContext is not inherited by members of the ObservableCollection.
edit:
Try setting the ShortcutKey.DataContext in onShortcutKeysChanged:
private static void onShortcutKeysChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var shortcuts = args.NewValue as ShortcutKeyCollection;
if (obj is FrameworkElement && shortcuts != null)
{
var element = obj as FrameworkElement;
ForEach(ShortcutKey sk in shortcuts)
{
sk.DataContext = element.DataContext;
element.KeyUp += (s, e) => sk.Command.Execute(null));
}
}
}
It looks like unless an object is inserted into the visual tree, no DataContext inheritance takes place, and thus no data binding works. I couldn't find a way to get the container's data context to be passed to the ShortcutKey objects, so as a workaround, I set up the binding in the code behind.
Hopefully someone else has a different answer that will show me how I won't have to resort to setting up this data binding in the code.
Related
Working Example with "Binding":
I have a UserControl which I use like this in my MainWindow:
<userControls:NoMarkupControl/>
The ViewModel of my MainWindow contains this property:
private string _exampleText = "example";
public string ExampleText
{
get { return _exampleText; }
set
{
_exampleText = value;
OnPropertyChanged();
}
}
inside the UserControl I bind my ViewModel to this property:
<TextBlock Text="{Binding ExampleText}"/>
as a result "example" gets displayed when I start the app. Everything works.
Not working example with Custom Markup Extension:
Now I have a MarkupExtension:
public class ExampleTextExtension : MarkupExtension
{
private static readonly List<DependencyProperty> StorageProperties = new List<DependencyProperty>();
private readonly object _parameter;
private DependencyProperty _dependencyProperty;
public ExampleTextExtension(object parameter)
{
_parameter = parameter;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
DependencyObject targetObject;
if (target?.TargetObject is DependencyObject dependencyObject &&
target.TargetProperty is DependencyProperty)
{
targetObject = dependencyObject;
}
else
{
return this;
}
_dependencyProperty = SetUnusedStorageProperty(targetObject, _parameter);
return GetLocalizedText((string)targetObject.GetValue(_dependencyProperty));
}
private static string GetLocalizedText(string text)
{
return text == null ? null : $"markup: {text}";
}
private static DependencyProperty SetUnusedStorageProperty(DependencyObject obj, object value)
{
var property = StorageProperties.FirstOrDefault(p => obj.ReadLocalValue(p) == DependencyProperty.UnsetValue);
if (property == null)
{
property = DependencyProperty.RegisterAttached("Storage" + StorageProperties.Count, typeof(object), typeof(ExampleTextExtension), new PropertyMetadata());
StorageProperties.Add(property);
}
if (value is MarkupExtension markupExtension)
{
var resolvedValue = markupExtension.ProvideValue(new ServiceProvider(obj, property));
obj.SetValue(property, resolvedValue);
}
else
{
obj.SetValue(property, value);
}
return property;
}
private class ServiceProvider : IServiceProvider, IProvideValueTarget
{
public object TargetObject { get; }
public object TargetProperty { get; }
public ServiceProvider(object targetObject, object targetProperty)
{
TargetObject = targetObject;
TargetProperty = targetProperty;
}
public object GetService(Type serviceType)
{
return serviceType.IsInstanceOfType(this) ? this : null;
}
}
}
Again I have a UserControl which I use like this in my MainWindow:
<userControls:MarkupControl/>
The ViewModel of my MainWindow stays the same like above.
inside the UserControl I bind to my TextBlock Text property like this:
<TextBlock Text="{markupExtensions:ExampleText {Binding ExampleText}}"/>
as a result my UserControl displays nothing. I would have expected to display "markup: example"
The binding somehow does not work in this case.
Does anybody know how to fix this?
Additional information:
it works when used like this (dependency property MarkupText is created in user control):
<userControls:MarkupControl MarkupText={markupExtensions:ExampleText {Binding ExampleText}}/>
<TextBlock Text="{Binding Text, ElementName=MarkupControl}"/>
Firstly, you need to refactor your extension to simplify the implementation. You don't need a static context here. Getting rid of the class context will make the tracking of the created attached properties obsolete. You can drop the related collection safely. In your case, it's more efficient to store values in an instance context. Attached properties are also a convenient solution to store values per instance especially in a static context.
Secondly, you got a timing issue. The first time the extension is called, the Binding is not initialized properly: it doesn't provide the final value of the Binding.Source.
Additionally, your current implementation does not support property changes.
To fix this, you would have to track the Binding.Target updates when a value is sent from the Binding.Source (for a default BindingMode.OneWay). You can achieve this by listening to the Binding.TargetUpdated event (as stated in my previous comment) or register a property changed handler with the attached property (recommended).
To support two way binding, you would also have to track the target property (the property your MarkupExtension is assigned to).
A fixed and improved version could look as follows:
public class ExampleTextExtension : MarkupExtension
{
private static DependencyProperty ResolvedBindingSourceValueProperty = DependencyProperty.RegisterAttached(
"ResolvedBindingSourceValue",
typeof(object),
typeof(ExampleTextExtension),
new PropertyMetadata(default(object), OnResolvedBindingSourceValueChanged));
// Use attached property to store the target object
// for reference from a static context without dealing with class level members that are shared between instances.
private static DependencyProperty TargetPropertyProperty = DependencyProperty.RegisterAttached(
"TargetProperty",
typeof(DependencyProperty),
typeof(ExampleTextExtension),
new PropertyMetadata(default));
private Binding Binding { get; }
// Accept BindingBase to support MultiBinding etc.
public ExampleTextExtension(Binding binding)
{
this.Binding = binding;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var provideValueTargetService = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (provideValueTargetService?.TargetObject is not DependencyObject targetObject
|| provideValueTargetService?.TargetProperty is not DependencyProperty targetProperty)
{
return this;
}
targetObject.SetValue(ExampleTextExtension.TargetPropertyProperty, targetProperty);
AttachBinding(targetObject);
return string.Empty;
}
private static string GetLocalizedText(string text)
=> String.IsNullOrWhiteSpace(text)
? string.Empty
: $"markup: {text}";
// By now, only supports OneWay binding
private void AttachBinding(DependencyObject targetObject)
{
switch (this.Binding.Mode)
{
case BindingMode.OneWay:
case BindingMode.Default:
HandleOneWayBinding(targetObject); break;
default: throw new NotSupportedException();
}
}
private void HandleOneWayBinding(DependencyObject targetObject)
{
BindingOperations.SetBinding(targetObject, ExampleTextExtension.ResolvedBindingSourceValueProperty, this.Binding);
}
// Property changed handler to update the target of this extension
// with the localized value
private static void OnResolvedBindingSourceValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
string localizedText = GetLocalizedText(e.NewValue as string);
var targetProperty = d.GetValue(ExampleTextExtension.TargetPropertyProperty) as DependencyProperty;
d.SetValue(targetProperty, localizedText);
}
}
Remarks
There are better solutions to introduce localization without compromising the general syntax or legacy code. For example, introducing this MarkupExtension to existing code will break this code as all relevant data bindings (C# and XAML) have to be modified.
The most common approach is to use satellite assemblies and localized resources. Instead of converting text values during data binding you should localize the value source directly (so that the Binding transfers already localized values).
In other words, make sure that the data source is localized. Let the binding source expose the text by fetching it from a localized repository.
I have a usercontrol which has a grid control inside
<UserControl x:Class="MyGrid">
<Telerik:RadGridView EnableRowVirtualization="false">
</Telerik:RadGridView/>
</UserControl>
How can I expose the EnableRowVirtualization property of the control inside the usercontrol using DependencyProperty so that when someone uses the MyGrid usercontrol, the user will just do something like this
<grids:MyGrid EnableRowVirtualization="false"> </grids:MyGrid>
UPDATE: Right now, this is just what I came up
public partial class MyGrid //myGrid userControl
{
public bool EnableRowVirtualization
{
get { return (bool)GetValue(EnableRowVirtualizationProperty); }
set { SetValue(EnableRowVirtualizationProperty, value); }
}
// Using a DependencyProperty as the backing store for EnableRowVirtualization. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EnableRowVirtualizationProperty =
DependencyProperty.Register("EnableRowVirtualization", typeof(bool), typeof(MaxGridView), new UIPropertyMetadata(false, OnEnableRowVirtualizationPropertyChanged)
);
private static void OnEnableRowVirtualizationPropertyChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var grid = (RadGridView)depObj;
if (grid != null)
{
grid.EnableRowVirtualization = (bool)e.NewValue;
}
}
If you give the Telerik grid a name you can then access it from the code of the dependency property. If you also combine that with an PropertyChanged property metadata when you define the dependency property then you can simply relay the value through to the underlying grid.
This is just off the top of my head, but something like this should do the trick:
public static readonly DependencyProperty EnableRowVirtualizationProperty =
DependencyProperty.Register("EnableRowVirtualization"
, typeof(bool)
, typeof(MyGrid)
, new UIPropertyMetadata(false, OnEnableRowVirtualizationPropertyChanged)
);
private static void OnEnableRowVirtualizationPropertyChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var myGrid = depObj as MyGrid;
if (myGrid != null)
{
myGrid.InnerTelerikGrid.EnableRowVirtualization = e.NewValue;
}
}
For more information check out DependencyProperty.RegisterAttached Method (String, Type, Type, PropertyMetadata) and UIPropertyMetadata Constructor (Object, PropertyChangedCallback).
I want to create an AttachedProperty of Type Collection, which contains references to other existing elements, as shown below:
<Window x:Class="myNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:myNamespace"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<ContentPresenter>
<ContentPresenter.Content>
<Button>
<local:DependencyObjectCollectionHost.Objects>
<local:DependencyObjectCollection>
<local:DependencyObjectContainer Object="{Binding ElementName=myButton}"/>
</local:DependencyObjectCollection>
</local:DependencyObjectCollectionHost.Objects>
</Button>
</ContentPresenter.Content>
</ContentPresenter>
<Button x:Name="myButton" Grid.Row="1"/>
</Grid>
</Window>
Therefore I've created a generic class, called ObjectContainer, to gain the possibility to do so with Binding:
public class ObjectContainer<T> : DependencyObject
where T : DependencyObject
{
static ObjectContainer()
{
ObjectProperty = DependencyProperty.Register
(
"Object",
typeof(T),
typeof(ObjectContainer<T>),
new PropertyMetadata(null)
);
}
public static DependencyProperty ObjectProperty;
[Bindable(true)]
public T Object
{
get { return (T)this.GetValue(ObjectProperty); }
set { this.SetValue(ObjectProperty, value); }
}
}
public class DependencyObjectContainer : ObjectContainer<DependencyObject> { }
public class DependencyObjectCollection : Collection<DependencyObjectContainer> { }
public static class DependencyObjectCollectionHost
{
static DependencyObjectCollectionHost()
{
ObjectsProperty = DependencyProperty.RegisterAttached
(
"Objects",
typeof(DependencyObjectCollection),
typeof(DependencyObjectCollectionHost),
new PropertyMetadata(null, OnObjectsChanged)
);
}
public static DependencyObjectCollection GetObjects(DependencyObject dependencyObject)
{
return (DependencyObjectCollection)dependencyObject.GetValue(ObjectsProperty);
}
public static void SetObjects(DependencyObject dependencyObject, DependencyObjectCollection value)
{
dependencyObject.SetValue(ObjectsProperty, value);
}
public static readonly DependencyProperty ObjectsProperty;
private static void OnObjectsChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var objects = (DependencyObjectCollection)e.NewValue;
if (objects.Count != objects.Count(d => d.Object != null))
throw new ArgumentException();
}
}
I'm not able to establish any binding within the Collection. I think I've already figured out, what the problem is. The elements in the Collection have no DataContext related to the Binding. However, I've no clue what I can do against it.
EDIT:
Fixed the missing Name Property of the Button.
Note: I know that the binding cannot work, because every Binding which doesn't declare a Source explicitly will use it's DataContext as it's Source. Like I already mentioned: We don't have such a DataContext within my Collection and there's no VisualTree where the non-existing FrameworkElement could be part of ;)
Maybe someone had a similiar problem in the past and found a suitable solution.
EDIT2 related to H.B.s post:
With the following change to the items within the collection it seems to work now:
<local:DependencyObjectContainer Object="{x:Reference myButton}"/>
Interesting behavior:
When the OnObjectsChanged Event-Handler is called, the collection contains zero elements ... I assume that's because the creation of the elements (done within the InitializeComponent method) hasn't finished yet.
Btw. As you H.B. said the use of the Container class is unnecessary when using x:Reference. Are there any disadvantages when using x:Reference which I don't see at the first moment?
EDIT3 Solution:
I've added a custom Attached Event in order to be notified, when the Collection changed.
public class DependencyObjectCollection : ObservableCollection<DependencyObject> { }
public static class ObjectHost
{
static KeyboardObjectHost()
{
ObjectsProperty = DependencyProperty.RegisterAttached
(
"Objects",
typeof(DependencyObjectCollection),
typeof(KeyboardObjectHost),
new PropertyMetadata(null, OnObjectsPropertyChanged)
);
ObjectsChangedEvent = EventManager.RegisterRoutedEvent
(
"ObjectsChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(KeyboardObjectHost)
);
}
public static DependencyObjectCollection GetObjects(DependencyObject dependencyObject)
{
return (DependencyObjectCollection)dependencyObject.GetValue(ObjectsProperty);
}
public static void SetObjects(DependencyObject dependencyObject, DependencyObjectCollection value)
{
dependencyObject.SetValue(ObjectsProperty, value);
}
public static void AddObjectsChangedHandler(DependencyObject dependencyObject, RoutedEventHandler h)
{
var uiElement = dependencyObject as UIElement;
if (uiElement != null)
uiElement.AddHandler(ObjectsChangedEvent, h);
else
throw new ArgumentException(string.Format("Cannot add handler to object of type: {0}", dependencyObject.GetType()), "dependencyObject");
}
public static void RemoveObjectsChangedHandler(DependencyObject dependencyObject, RoutedEventHandler h)
{
var uiElement = dependencyObject as UIElement;
if (uiElement != null)
uiElement.RemoveHandler(ObjectsChangedEvent, h);
else
throw new ArgumentException(string.Format("Cannot remove handler from object of type: {0}", dependencyObject.GetType()), "dependencyObject");
}
public static bool CanControlledByKeyboard(DependencyObject dependencyObject)
{
var objects = GetObjects(dependencyObject);
return objects != null && objects.Count != 0;
}
public static readonly DependencyProperty ObjectsProperty;
public static readonly RoutedEvent ObjectsChangedEvent;
private static void OnObjectsPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
Observable.FromEvent<NotifyCollectionChangedEventArgs>(e.NewValue, "CollectionChanged")
.DistinctUntilChanged()
.Subscribe(args =>
{
var objects = (DependencyObjectCollection)args.Sender;
if (objects.Count == objects.Count(d => d != null)
OnObjectsChanged(dependencyObject);
else
throw new ArgumentException();
});
}
private static void OnObjectsChanged(DependencyObject dependencyObject)
{
RaiseObjectsChanged(dependencyObject);
}
private static void RaiseObjectsChanged(DependencyObject dependencyObject)
{
var uiElement = dependencyObject as UIElement;
if (uiElement != null)
uiElement.RaiseEvent(new RoutedEventArgs(ObjectsChangedEvent));
}
}
You can use x:Reference in .NET 4, it's "smarter" than ElementName and unlike bindings it does not require the target to be a dependency property.
You can even get rid of the container class, but your property needs to have the right type so the ArrayList can directly convert to the property value instead of adding the whole list as an item. Using x:References directly will not work.
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
<local:AttachedProperties.Objects>
<col:ArrayList>
<x:Reference>button1</x:Reference>
<x:Reference>button2</x:Reference>
</col:ArrayList>
</local:AttachedProperties.Objects>
public static readonly DependencyProperty ObjectsProperty =
DependencyProperty.RegisterAttached
(
"Objects",
typeof(IList),
typeof(FrameworkElement),
new UIPropertyMetadata(null)
);
public static IList GetObjects(DependencyObject obj)
{
return (IList)obj.GetValue(ObjectsProperty);
}
public static void SetObjects(DependencyObject obj, IList value)
{
obj.SetValue(ObjectsProperty, value);
}
Further writing the x:References as
<x:Reference Name="button1"/>
<x:Reference Name="button2"/>
will cause some more nice errors.
I think the answer can be found in the following two links:
Binding.ElementName Property
XAML Namescopes and Name-related APIs
Especially the second states:
FrameworkElement has FindName, RegisterName and UnregisterName methods. If the object you call these methods on owns a XAML namescope, the methods call into the methods of the relevant XAML namescope. Otherwise, the parent element is checked to see if it owns a XAML namescope, and this process continues recursively until a XAML namescope is found (because of the XAML processor behavior, there is guaranteed to be a XAML namescope at the root). FrameworkContentElement has analogous behaviors, with the exception that no FrameworkContentElement will ever own a XAML namescope. The methods exist on FrameworkContentElement so that the calls can be forwarded eventually to a FrameworkElement parent element.
So the issue in your sample caused by the fact that your classes are DependencyObjects at most but none of them is FrameworkElement. Not being a FrameworkElement it cannot provide Parent property to resolve name specified in Binding.ElementName.
But this isn't end. In order to resolve names from Binding.ElementName your container not only should be a FrameworkElement but it should also have FrameworkElement.Parent. Populating attached property doesn't set this property, your instance should be a logical child of your button so it will be able to resolve the name.
So I had to make some changes into your code in order to make it working (resolving ElementName), but at this state I do not think it meets your needs. I'm pasting the code below so you can play with it.
public class ObjectContainer<T> : FrameworkElement
where T : DependencyObject
{
static ObjectContainer()
{
ObjectProperty = DependencyProperty.Register("Object", typeof(T), typeof(ObjectContainer<T>), null);
}
public static DependencyProperty ObjectProperty;
[Bindable(true)]
public T Object
{
get { return (T)this.GetValue(ObjectProperty); }
set { this.SetValue(ObjectProperty, value); }
}
}
public class DependencyObjectContainer : ObjectContainer<DependencyObject> { }
public class DependencyObjectCollection : FrameworkElement
{
private object _child;
public Object Child
{
get { return _child; }
set
{
_child = value;
AddLogicalChild(_child);
}
}
}
public static class DependencyObjectCollectionHost
{
static DependencyObjectCollectionHost()
{
ObjectsProperty = DependencyProperty.RegisterAttached
(
"Objects",
typeof(DependencyObjectCollection),
typeof(DependencyObjectCollectionHost),
new PropertyMetadata(null, OnObjectsChanged)
);
}
public static DependencyObjectCollection GetObjects(DependencyObject dependencyObject)
{
return (DependencyObjectCollection)dependencyObject.GetValue(ObjectsProperty);
}
public static void SetObjects(DependencyObject dependencyObject, DependencyObjectCollection value)
{
dependencyObject.SetValue(ObjectsProperty, value);
}
public static readonly DependencyProperty ObjectsProperty;
private static void OnObjectsChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
((Button) dependencyObject).Content = e.NewValue;
var objects = (DependencyObjectCollection)e.NewValue;
// this check doesn't work anyway. d.Object was populating later than this check was performed
// if (objects.Count != objects.Count(d => d.Object != null))
// throw new ArgumentException();
}
}
Probably you still can make this working by implementing INameScope interface and its FindName method particularly but I haven't tried doing that.
I've created a custom control with, amongst others, the following:
public partial class MyButton : UserControl
{
public bool Enabled
{
get { return (bool)GetValue(EnabledProperty); }
set {
SetValue(EnabledProperty, value);
SomeOtherStuff();
}
}
}
public static readonly DependencyProperty EnabledProperty =
DependencyProperty.Register("Enabled", typeof(bool), typeof(MyButton), new PropertyMetadata(true));
public static void SetEnabled(DependencyObject obj, bool value)
{
obj.SetValue(EnabledProperty, value);
}
public static bool GetEnabled(DependencyObject obj)
{
return (bool) obj.GetValue(EnabledProperty);
}
}
In my XAML, I (try to) use binding to set the Enabled property:
<MyButton x:Name="myButtom1" Enabled="{Binding CanEnableButton}"/>
I know the bind between my control and the underlying data model is valid and working as I can bind 'IsEnabled' (a native property of the underlying UserControl) and it works as expected. However, my Enabled property is never set via the above binding. I've put breakpoints on my property set/get and they never get hit at all.
I can only imaging I've missed something relating to binding in my custom control. Can anyone see what?
I've tried implementing INotifyPropertyChanged on my control (and calling the PropertyChanged event from my Enabled setter) ... but that didn't fix it.
[ BTW: In case you are wondering "Why?": I can't intercept changes to the IsEnabled state of the base control, so I decided to implement and use my own version of a Enable/disable property (which I called Enabled) - one where I could plug my own code into the property setter ]
First of all drop the SetEnabled and GetEnabled pair, these only make sense for an attached property which is not what you are doing.
Now your main problem is that you are under the false assumption that the get/set members of your propery get called during binding, they don't.
What you need is to pass a call back method in the property meta data, it's here that you intercept changes and take other actions like so:-
public bool IsEnabled
{
get { return (bool)GetValue(IsEnabledProperty); }
set { SetValue(IsEnabledProperty, value); }
}
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.Register(
"IsEnabled",
typeof(bool),
typeof(MyButton),
new PropertyMetadata(true, OnIsEnabledPropertyChanged));
private static void OnIsEnabledPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MyButton source = d as MyButton;
source.SomeOtherStuff();
}
private void SomeOtherStuff()
{
// Your other stuff here
}
With this in place regardless of how the propery is changed the SomeOtherStuff procedure will execute.
I'd suggest using the IsEnabledChanged event which is part of every Control/UserControl.
That would allow you to hook up to the event and do whatever actions you want to take.
public MainPage()
{
InitializeComponent();
this.IsEnabledChanged += new DependencyPropertyChangedEventHandler(MainPage_IsEnabledChanged);
}
void MainPage_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// Do SomeStuff
}
I want to write a ViewModel that always knows the current state of some read-only dependency properties from the View.
Specifically, my GUI contains a FlowDocumentPageViewer, which displays one page at a time from a FlowDocument. FlowDocumentPageViewer exposes two read-only dependency properties called CanGoToPreviousPage and CanGoToNextPage. I want my ViewModel to always know the values of these two View properties.
I figured I could do this with a OneWayToSource databinding:
<FlowDocumentPageViewer
CanGoToNextPage="{Binding NextPageAvailable, Mode=OneWayToSource}" ...>
If this was allowed, it would be perfect: whenever the FlowDocumentPageViewer's CanGoToNextPage property changed, the new value would get pushed down into the ViewModel's NextPageAvailable property, which is exactly what I want.
Unfortunately, this doesn't compile: I get an error saying 'CanGoToPreviousPage' property is read-only and cannot be set from markup. Apparently read-only properties don't support any kind of databinding, not even databinding that's read-only with respect to that property.
I could make my ViewModel's properties be DependencyProperties, and make a OneWay binding going the other way, but I'm not crazy about the separation-of-concerns violation (ViewModel would need a reference to the View, which MVVM databinding is supposed to avoid).
FlowDocumentPageViewer doesn't expose a CanGoToNextPageChanged event, and I don't know of any good way to get change notifications from a DependencyProperty, short of creating another DependencyProperty to bind it to, which seems like overkill here.
How can I keep my ViewModel informed of changes to the view's read-only properties?
Yes, I've done this in the past with the ActualWidth and ActualHeight properties, both of which are read-only. I created an attached behavior that has ObservedWidth and ObservedHeight attached properties. It also has an Observe property that is used to do the initial hook-up. Usage looks like this:
<UserControl ...
SizeObserver.Observe="True"
SizeObserver.ObservedWidth="{Binding Width, Mode=OneWayToSource}"
SizeObserver.ObservedHeight="{Binding Height, Mode=OneWayToSource}"
So the view model has Width and Height properties that are always in sync with the ObservedWidth and ObservedHeight attached properties. The Observe property simply attaches to the SizeChanged event of the FrameworkElement. In the handle, it updates its ObservedWidth and ObservedHeight properties. Ergo, the Width and Height of the view model is always in sync with the ActualWidth and ActualHeight of the UserControl.
Perhaps not the perfect solution (I agree - read-only DPs should support OneWayToSource bindings), but it works and it upholds the MVVM pattern. Obviously, the ObservedWidth and ObservedHeight DPs are not read-only.
UPDATE: here's code that implements the functionality described above:
public static class SizeObserver
{
public static readonly DependencyProperty ObserveProperty = DependencyProperty.RegisterAttached(
"Observe",
typeof(bool),
typeof(SizeObserver),
new FrameworkPropertyMetadata(OnObserveChanged));
public static readonly DependencyProperty ObservedWidthProperty = DependencyProperty.RegisterAttached(
"ObservedWidth",
typeof(double),
typeof(SizeObserver));
public static readonly DependencyProperty ObservedHeightProperty = DependencyProperty.RegisterAttached(
"ObservedHeight",
typeof(double),
typeof(SizeObserver));
public static bool GetObserve(FrameworkElement frameworkElement)
{
frameworkElement.AssertNotNull("frameworkElement");
return (bool)frameworkElement.GetValue(ObserveProperty);
}
public static void SetObserve(FrameworkElement frameworkElement, bool observe)
{
frameworkElement.AssertNotNull("frameworkElement");
frameworkElement.SetValue(ObserveProperty, observe);
}
public static double GetObservedWidth(FrameworkElement frameworkElement)
{
frameworkElement.AssertNotNull("frameworkElement");
return (double)frameworkElement.GetValue(ObservedWidthProperty);
}
public static void SetObservedWidth(FrameworkElement frameworkElement, double observedWidth)
{
frameworkElement.AssertNotNull("frameworkElement");
frameworkElement.SetValue(ObservedWidthProperty, observedWidth);
}
public static double GetObservedHeight(FrameworkElement frameworkElement)
{
frameworkElement.AssertNotNull("frameworkElement");
return (double)frameworkElement.GetValue(ObservedHeightProperty);
}
public static void SetObservedHeight(FrameworkElement frameworkElement, double observedHeight)
{
frameworkElement.AssertNotNull("frameworkElement");
frameworkElement.SetValue(ObservedHeightProperty, observedHeight);
}
private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var frameworkElement = (FrameworkElement)dependencyObject;
if ((bool)e.NewValue)
{
frameworkElement.SizeChanged += OnFrameworkElementSizeChanged;
UpdateObservedSizesForFrameworkElement(frameworkElement);
}
else
{
frameworkElement.SizeChanged -= OnFrameworkElementSizeChanged;
}
}
private static void OnFrameworkElementSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateObservedSizesForFrameworkElement((FrameworkElement)sender);
}
private static void UpdateObservedSizesForFrameworkElement(FrameworkElement frameworkElement)
{
// WPF 4.0 onwards
frameworkElement.SetCurrentValue(ObservedWidthProperty, frameworkElement.ActualWidth);
frameworkElement.SetCurrentValue(ObservedHeightProperty, frameworkElement.ActualHeight);
// WPF 3.5 and prior
////SetObservedWidth(frameworkElement, frameworkElement.ActualWidth);
////SetObservedHeight(frameworkElement, frameworkElement.ActualHeight);
}
}
I use a universal solution which works not only with ActualWidth and ActualHeight, but also with any data you can bind to at least in reading mode.
The markup looks like this, provided ViewportWidth and ViewportHeight are properties of the view model
<Canvas>
<u:DataPiping.DataPipes>
<u:DataPipeCollection>
<u:DataPipe Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualWidth}"
Target="{Binding Path=ViewportWidth, Mode=OneWayToSource}"/>
<u:DataPipe Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualHeight}"
Target="{Binding Path=ViewportHeight, Mode=OneWayToSource}"/>
</u:DataPipeCollection>
</u:DataPiping.DataPipes>
<Canvas>
Here is the source code for the custom elements
public class DataPiping
{
#region DataPipes (Attached DependencyProperty)
public static readonly DependencyProperty DataPipesProperty =
DependencyProperty.RegisterAttached("DataPipes",
typeof(DataPipeCollection),
typeof(DataPiping),
new UIPropertyMetadata(null));
public static void SetDataPipes(DependencyObject o, DataPipeCollection value)
{
o.SetValue(DataPipesProperty, value);
}
public static DataPipeCollection GetDataPipes(DependencyObject o)
{
return (DataPipeCollection)o.GetValue(DataPipesProperty);
}
#endregion
}
public class DataPipeCollection : FreezableCollection<DataPipe>
{
}
public class DataPipe : Freezable
{
#region Source (DependencyProperty)
public object Source
{
get { return (object)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register("Source", typeof(object), typeof(DataPipe),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnSourceChanged)));
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DataPipe)d).OnSourceChanged(e);
}
protected virtual void OnSourceChanged(DependencyPropertyChangedEventArgs e)
{
Target = e.NewValue;
}
#endregion
#region Target (DependencyProperty)
public object Target
{
get { return (object)GetValue(TargetProperty); }
set { SetValue(TargetProperty, value); }
}
public static readonly DependencyProperty TargetProperty =
DependencyProperty.Register("Target", typeof(object), typeof(DataPipe),
new FrameworkPropertyMetadata(null));
#endregion
protected override Freezable CreateInstanceCore()
{
return new DataPipe();
}
}
If anyone else is interested, I coded up an approximation of Kent's solution here:
class SizeObserver
{
#region " Observe "
public static bool GetObserve(FrameworkElement elem)
{
return (bool)elem.GetValue(ObserveProperty);
}
public static void SetObserve(
FrameworkElement elem, bool value)
{
elem.SetValue(ObserveProperty, value);
}
public static readonly DependencyProperty ObserveProperty =
DependencyProperty.RegisterAttached("Observe", typeof(bool), typeof(SizeObserver),
new UIPropertyMetadata(false, OnObserveChanged));
static void OnObserveChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
FrameworkElement elem = depObj as FrameworkElement;
if (elem == null)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
elem.SizeChanged += OnSizeChanged;
else
elem.SizeChanged -= OnSizeChanged;
}
static void OnSizeChanged(object sender, RoutedEventArgs e)
{
if (!Object.ReferenceEquals(sender, e.OriginalSource))
return;
FrameworkElement elem = e.OriginalSource as FrameworkElement;
if (elem != null)
{
SetObservedWidth(elem, elem.ActualWidth);
SetObservedHeight(elem, elem.ActualHeight);
}
}
#endregion
#region " ObservedWidth "
public static double GetObservedWidth(DependencyObject obj)
{
return (double)obj.GetValue(ObservedWidthProperty);
}
public static void SetObservedWidth(DependencyObject obj, double value)
{
obj.SetValue(ObservedWidthProperty, value);
}
// Using a DependencyProperty as the backing store for ObservedWidth. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ObservedWidthProperty =
DependencyProperty.RegisterAttached("ObservedWidth", typeof(double), typeof(SizeObserver), new UIPropertyMetadata(0.0));
#endregion
#region " ObservedHeight "
public static double GetObservedHeight(DependencyObject obj)
{
return (double)obj.GetValue(ObservedHeightProperty);
}
public static void SetObservedHeight(DependencyObject obj, double value)
{
obj.SetValue(ObservedHeightProperty, value);
}
// Using a DependencyProperty as the backing store for ObservedHeight. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ObservedHeightProperty =
DependencyProperty.RegisterAttached("ObservedHeight", typeof(double), typeof(SizeObserver), new UIPropertyMetadata(0.0));
#endregion
}
Feel free to use it in your apps. It works well. (Thanks Kent!)
Here is another solution to this "bug" which I blogged about here:
OneWayToSource Binding for ReadOnly Dependency Property
It works by using two Dependency Properties, Listener and Mirror. Listener is bound OneWay to the TargetProperty and in the PropertyChangedCallback it updates the Mirror property which is bound OneWayToSource to whatever was specified in the Binding. I call it PushBinding and it can be set on any read-only Dependency Property like this
<TextBlock Name="myTextBlock"
Background="LightBlue">
<pb:PushBindingManager.PushBindings>
<pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
<pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
</pb:PushBindingManager.PushBindings>
</TextBlock>
Download Demo Project Here.
It contains source code and short sample usage.
One last note, since .NET 4.0 we are even further away from built-in-support for this, since a OneWayToSource Binding reads the value back from the Source after it has updated it
I like Dmitry Tashkinov's solution!
However it crashed my VS in design mode. That's why I added a line to OnSourceChanged method:
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
((DataPipe)d).OnSourceChanged(e);
}
I think it can be done a bit simpler:
xaml:
behavior:ReadOnlyPropertyToModelBindingBehavior.ReadOnlyDependencyProperty="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
behavior:ReadOnlyPropertyToModelBindingBehavior.ModelProperty="{Binding MyViewModelProperty}"
cs:
public class ReadOnlyPropertyToModelBindingBehavior
{
public static readonly DependencyProperty ReadOnlyDependencyPropertyProperty = DependencyProperty.RegisterAttached(
"ReadOnlyDependencyProperty",
typeof(object),
typeof(ReadOnlyPropertyToModelBindingBehavior),
new PropertyMetadata(OnReadOnlyDependencyPropertyPropertyChanged));
public static void SetReadOnlyDependencyProperty(DependencyObject element, object value)
{
element.SetValue(ReadOnlyDependencyPropertyProperty, value);
}
public static object GetReadOnlyDependencyProperty(DependencyObject element)
{
return element.GetValue(ReadOnlyDependencyPropertyProperty);
}
private static void OnReadOnlyDependencyPropertyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
SetModelProperty(obj, e.NewValue);
}
public static readonly DependencyProperty ModelPropertyProperty = DependencyProperty.RegisterAttached(
"ModelProperty",
typeof(object),
typeof(ReadOnlyPropertyToModelBindingBehavior),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static void SetModelProperty(DependencyObject element, object value)
{
element.SetValue(ModelPropertyProperty, value);
}
public static object GetModelProperty(DependencyObject element)
{
return element.GetValue(ModelPropertyProperty);
}
}