Binding within an AttachedProperty of Type Collection to an other Element
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 st开发者_开发知识库atic 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.
精彩评论