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.