开发者

How to use binding to bind to the grand-parent element in Silverlight?

开发者 https://www.devze.com 2022-12-21 06:16 出处:网络
This feels like it should be such an easy solution, but I think I\'m crippled by thinking about the problem in WPF terms.

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". 开发者_JAVA技巧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.)


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>


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消