views:

244

answers:

2

This feels like it should be such an easy solution, but I think I'm crippled by thinking about the problem in WPF terms.

In my view Model I have patterns where a container has a collection of items (e.g. Groups and Users). So I create 3 classes, "Group", "User" and "UserCollection". Within XAML, I'm using an ItemsControl to repeat all the users, e.g.:

<StackPanel DataContext="{Binding CurrentGroup}">
  <ItemsControl ItemsSource="{Binding UsersInGroup}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding UserName"></TextBlock>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</StackPanel>

Now, within the DataTemplate, I want to bind to en element in the CurrentGroup. Within WPF, I would use FindAncestor such as:

<TextBlock Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Group}}, Path=GroupName}"></TextBlock>

How, in Silverlight, can I make the binding to a grandparent's property? I'm guessing that there is an easy way that I can't see.

(I wish I had learned Silverlight first rather than WPF. That way I wouldn't keep trying to use WPF specific solutions in Silverlight apps.)

+1  A: 

Yeah, SL is great, but it is hard to use it after learning WPF, exactly like you write.

I don't have a solution for the general problem.

For this particular one, since you have a view model, can you have a back pointer to the group from the user ? If users can belong to different groups, this means creating a specific copy for each UserCollection.

Timores
I see your point and that would make a reasonable work-around. Still hoping for a generic solution though...
Adrian
100% agree, maybe SL 5...
Timores
+2  A: 

Yeah, unfortunately the RelativeSource markup extension is still kind of crippled in Silverlight...all it supports is TemplatedParent and Self, if memory serves. And since Silverlight doesn't support user-created markup extensions (yet), there's no direct analog to the FindAncestor syntax.

Now realizing that was a useless comment, let's see if we can figure out a different way of doing it. I imagine the problem with directly porting the FindAncestor syntax from WPF to silverlight has to do with the fact Silverlight doesn't have a true Logical Tree. I wonder if you could use a ValueConverter or Attached Behavior to create a "VisualTree-walking" analog...

(some googling occurs)

Hey, looks like someone else tried doing this in Silverlight 2.0 to implement ElementName - it might be a good start for a workaround: http://www.scottlogic.co.uk/blog/colin/2009/02/relativesource-binding-in-silverlight/

EDIT: Ok, here you go - proper credit should be given to the above author, but I've tweaked it to remove some bugs, etc - still loads of room for improvements:

    public class BindingProperties
{
    public string SourceProperty { get; set; }
    public string ElementName { get; set; }
    public string TargetProperty { get; set; }
    public IValueConverter Converter { get; set; }
    public object ConverterParameter { get; set; }
    public bool RelativeSourceSelf { get; set; }
    public BindingMode Mode { get; set; }
    public string RelativeSourceAncestorType { get; set; }
    public int RelativeSourceAncestorLevel { get; set; }

    public BindingProperties()
    {
        RelativeSourceAncestorLevel = 1;
    }
}

public static class BindingHelper
{
    public class ValueObject : INotifyPropertyChanged
    {
        private object _value;

        public object Value
        {
            get { return _value; }
            set
            {
                _value = value;
                OnPropertyChanged("Value");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    public static BindingProperties GetBinding(DependencyObject obj)
    {
        return (BindingProperties)obj.GetValue(BindingProperty);
    }

    public static void SetBinding(DependencyObject obj, BindingProperties value)
    {
        obj.SetValue(BindingProperty, value);
    }

    public static readonly DependencyProperty BindingProperty =
        DependencyProperty.RegisterAttached("Binding", typeof(BindingProperties), typeof(BindingHelper),
        new PropertyMetadata(null, OnBinding));


    /// <summary>
    /// property change event handler for BindingProperty
    /// </summary>
    private static void OnBinding(
        DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement targetElement = depObj as FrameworkElement;

        targetElement.Loaded += new RoutedEventHandler(TargetElement_Loaded);
    }

    private static void TargetElement_Loaded(object sender, RoutedEventArgs e)
    {
        FrameworkElement targetElement = sender as FrameworkElement;

        // get the value of our attached property
        BindingProperties bindingProperties = GetBinding(targetElement);

        if (bindingProperties.ElementName != null)
        {
            // perform our 'ElementName' lookup
            FrameworkElement sourceElement = targetElement.FindName(bindingProperties.ElementName) as FrameworkElement;

            // bind them
            CreateRelayBinding(targetElement, sourceElement, bindingProperties);
        }
        else if (bindingProperties.RelativeSourceSelf)
        {
            // bind an element to itself.
            CreateRelayBinding(targetElement, targetElement, bindingProperties);
        }
        else if (!string.IsNullOrEmpty(bindingProperties.RelativeSourceAncestorType))
        {
            Type ancestorType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(
                t => t.Name.Contains(bindingProperties.RelativeSourceAncestorType));

            if(ancestorType == null)
            {
                ancestorType = Assembly.GetCallingAssembly().GetTypes().FirstOrDefault(
                                    t => t.Name.Contains(bindingProperties.RelativeSourceAncestorType));                    
            }
            // navigate up the tree to find the type
            DependencyObject currentObject = targetElement;

            int currentLevel = 0;
            while (currentLevel < bindingProperties.RelativeSourceAncestorLevel)
            {
                do
                {
                    currentObject = VisualTreeHelper.GetParent(currentObject);
                    if(currentObject.GetType().IsSubclassOf(ancestorType))
                    {
                        break;
                    }
                }
                while (currentObject.GetType().Name != bindingProperties.RelativeSourceAncestorType);
                currentLevel++;
            }

            FrameworkElement sourceElement = currentObject as FrameworkElement;

            // bind them
            CreateRelayBinding(targetElement, sourceElement, bindingProperties);
        }
    }

    private static readonly BindingFlags dpFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;

    private struct RelayBindingKey
    {
        public DependencyProperty dependencyObject;
        public FrameworkElement frameworkElement;
    }

    /// <summary>
    /// A cache of relay bindings, keyed by RelayBindingKey which specifies a property of a specific 
    /// framework element.
    /// </summary>
    private static Dictionary<RelayBindingKey, ValueObject> relayBindings = new Dictionary<RelayBindingKey, ValueObject>();

    /// <summary>
    /// Creates a relay binding between the two given elements using the properties and converters
    /// detailed in the supplied bindingProperties.
    /// </summary>
    private static void CreateRelayBinding(FrameworkElement targetElement, FrameworkElement sourceElement,
        BindingProperties bindingProperties)
    {

        string sourcePropertyName = bindingProperties.SourceProperty + "Property";
        string targetPropertyName = bindingProperties.TargetProperty + "Property";

        // find the source dependency property
        FieldInfo[] sourceFields = sourceElement.GetType().GetFields(dpFlags);
        FieldInfo sourceDependencyPropertyField = sourceFields.First(i => i.Name == sourcePropertyName);
        DependencyProperty sourceDependencyProperty = sourceDependencyPropertyField.GetValue(null) as DependencyProperty;

        // find the target dependency property
        FieldInfo[] targetFields = targetElement.GetType().GetFields(dpFlags);
        FieldInfo targetDependencyPropertyField = targetFields.First(i => i.Name == targetPropertyName);
        DependencyProperty targetDependencyProperty = targetDependencyPropertyField.GetValue(null) as DependencyProperty;


        ValueObject relayObject;
        bool relayObjectBoundToSource = false;

        // create a key that identifies this source binding
        RelayBindingKey key = new RelayBindingKey()
        {
            dependencyObject = sourceDependencyProperty,
            frameworkElement = sourceElement
        };

        // do we already have a binding to this property?
        if (relayBindings.ContainsKey(key))
        {
            relayObject = relayBindings[key];
            relayObjectBoundToSource = true;
        }
        else
        {
            // create a relay binding between the two elements
            relayObject = new ValueObject();
        }


        // initialise the relay object with the source dependency property value 
        relayObject.Value = sourceElement.GetValue(sourceDependencyProperty);

        // create the binding for our target element to the relay object, this binding will
        // include the value converter
        Binding targetToRelay = new Binding();
        targetToRelay.Source = relayObject;
        targetToRelay.Path = new PropertyPath("Value");
        targetToRelay.Mode = bindingProperties.Mode;
        targetToRelay.Converter = bindingProperties.Converter;
        targetToRelay.ConverterParameter = bindingProperties.ConverterParameter;

        // set the binding on our target element
        targetElement.SetBinding(targetDependencyProperty, targetToRelay);

        if (!relayObjectBoundToSource && bindingProperties.Mode == BindingMode.TwoWay)
        {
            // create the binding for our source element to the relay object
            Binding sourceToRelay = new Binding();
            sourceToRelay.Source = relayObject;
            sourceToRelay.Path = new PropertyPath("Value");
            sourceToRelay.Converter = bindingProperties.Converter;
            sourceToRelay.ConverterParameter = bindingProperties.ConverterParameter;
            sourceToRelay.Mode = bindingProperties.Mode;

            // set the binding on our source element
            sourceElement.SetBinding(sourceDependencyProperty, sourceToRelay);

            relayBindings.Add(key, relayObject);
        }
    }
}

You'd use this thusly:

<TextBlock>
    <SilverlightApplication1:BindingHelper.Binding>
        <SilverlightApplication1:BindingProperties 
            TargetProperty="Text"
            SourceProperty="ActualWidth" 
            RelativeSourceAncestorType="UserControl"
            Mode="OneWay"
            />
    </SilverlightApplication1:BindingHelper.Binding>
</TextBlock>
JerKimball
Not exactly what I was looking for, but it's beginning to appear that what I want doesn't exist. This mechanism looks similar to some mechanisms for adding property Two-Way binding to ASP.NET that I've seen around. I thought when I wrote this question that I was having a blonde day and was just missing something trivial, obviously not!
Adrian