Binding to a ContentPresenter's visual elements/children from outside

First a brief "abstracted" short version of my problem. Probably not needed to discuss a solution, but below some further "optional" infos of the real underlying problem I'm having, just to understand the context.

So: I have a ContentPresenter using a DataTemplate to generate its layout for bound items. Now, outside of this contentpresenter, I'm trying to bind within an Element by name within that content presenter.

Assume the following Pseudo-XAML (MainTextBlock's binding won't work in practice):

   <TextBlock Text="{Binding Text, ElementName=MyTextblock, Source = ???}" DataContext="{x:Reference TheContentPresenter}" x:Name="MainTextblock"/>

    <ContentPresenter Content="{Binding SomeItem}" x:Name="TheContentPresenter">
        <ContentPresenter.ContentTemplate>
            <DataTemplate>
                <TextBlock x:Name="MyTextblock" Text="Test"/>
            </DataTemplate>
        </ContentPresenter.ContentTemplate>
    </ContentPresenter>

!! Please assume that the DataContext of MainTextblock MUST be (a reference to) TheContentPresenter !!

Given that assumption, how can I make the binding on MainTextblock work?

I can't bind to the ContentPresenter's Content property, because that contains the bound element (eg SomeItem), not its visual representation. Unfortunately, ContentPresenter doesn't seem to have any property representing its Visual Tree / Visual Children.

Is there any way to do this?


Now what do I really need this for? Feel free to skip reading this, it shouldn't be needed to discuss a solution of above's problem I believe.

I'm writing a behavior that adds customizable filters to a DataGrid:

<DataGrid AutoGenerateColumns="False">

        <i:Interaction.Behaviors>
            <filter:FilterBehavior>
                <filter:StringFilter Column="{x:Reference FirstCol}" Binding="{Binding DataContext.Value1}"/>
                <filter:StringFilter Column="{x:Reference SecondCol}" Binding="{??????? bind to Content -> Visual Children -> Element With Name "MyTextBlock" -> Property "Text"}"/>
            </filter:FilterBehavior>
        </i:Interaction.Behaviors>


        <DataGrid.Columns>
            <DataGridTextColumn x:Name="FirstCol" Header="Test" Binding="{Binding Value1}"/>
            <DataGridTemplateColumn x:Name="SecondCol" Header="Test 2">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock x:Name="MyTextblock" Text="{Binding Value2}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
</DataGrid>

"FilterBehavior" contains individual filters for each column, eg the first of them would be a filter that allows to search text within whatever column its bound to (FirstCol in this case), and hides columns where that text doesn't appear.

Now, Binding is the interesting part. The Binding property is of type BindingBase (so the binding is "deferred"). It's intended to define the value which is used for filtering. When filtering is supposed to occur, each filter loops through all DataGridCells of the column its bound to. For each DataGridCell, it sets the Binding's DataContext to the respective DataGridCell, and evaluates the binding.

So, the StringFilter would loop through each DataGridCell in FirstCol. For Each of them, it would retreive BindingBase "Binding" (ie {Binding DataContext.Value1}), set its DataContext to the DataGridCell, and evaluate this. So in that case, it would bind to WpfGridCell.DataContext.Value1, or in other words to the Value1 property of the item the DataGridCell contains. Later on, it will check if these evaluated items match the String that the user entered for filtering.

This works just fine.

However, I'm having trouble when trying to bind to the DataGridCell's visual content, as in the case of the second StringFilter with Column="{x:Reference SecondCol}". SecondCol is a DataGridTemplateColumn. It's cells content will be a ContentPresenter, whose template is DataGridTemplateColumn.CellTemplate, and whose Content is the element that the cell contains.

And this is where we get back to my simplified version from above. I now need to evaluate "Binding" with DataContext = DataGridCell, and somehow come up with a binding that let's me bind to the visual elements of the ContentPresenter that is given in DataGridCell.Content.

Thanks!


Since no other solution came up so far / this doesn't appear to be possible with XAML only, here's my current solution. Seems a bit messy, but it works and allows for relatively general use.

Essentially, I've introduced a second property to my filters called "BindingContext", also of type BindingBase. The consumer can either leave this at null, in which case it'll default to the respective DataGridCell, or it can assign a binding (which itself will get the DataContext = DataGridCell). This binding will be evaluated, and the result of which will be used as the datacontext for the "Binding" property:

                <filter:StringFilter Column="{x:Reference SecondCol}"
                           BindingContext="{Binding Content, Converter={StaticResource ContentPresenterToVisualHelperConverter}, ConverterParameter='MyTextblock'}" 
                           Binding="{Binding Visual.Text, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"                                          
                         />

Now I've created an IValueConverter which converts a ContentPresenter into a wrapper class, which itselfs exposes a "Visual" property. Depending on the use case, this visual property either exposes the ContentPresenters' first and only immediate visual child, or it finds a visual child by name. I've cached instantiation of the helper class, because otherwise the converter would create quite a lot of these, and each time it'd query the Visual Tree at least once.

It tries to keep this property synchronized to the ContentPresenter; while I don't t hink there's any direct way to monitor if its Visual Tree changes, I went with updating whenever the ContentPresenter's Content property changes. (Another way might be to update whenever its layout changes, but this obviously gets triggered a LOT in various cases, so seemed like overkill)

[ValueConversion(typeof(ContentPresenter), typeof(ContentPresenterVisualHelper))]
public class ContentPresenterToVisualHelperConverter : IValueConverter
{
    /// <param name="parameter">
    /// 1. Can be null/empty, in which case the first Visual Child of the ContentPresenter is returned by the Helper
    /// 2. Can be a string, in which case the ContentPresenter's child with the given name is returned
    /// </param>
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null)
            return null;

        ContentPresenter cp = value as ContentPresenter;

        if (cp == null)
            throw new InvalidOperationException(String.Format("value must be of type ContentPresenter, but was {0}", value.GetType().FullName));

        return ContentPresenterVisualHelper.GetInstance(cp, parameter as string);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

/// <summary>
/// Exposes either
/// A) A ContentPresenter's only immediate visual child, or
/// B) Any of the ContentPresenter's visual children by Name
/// in the ContentPresenterVisualHelper's "Visual" property. Implements INotifyPropertyChanged to notify when this visual is replaced.
/// </summary>
public class ContentPresenterVisualHelper : BindableBase, IDisposable
{
    private static object CacheLock = new object();
    private static MemoryCache Cache = new MemoryCache("ContentPresenterVisualHelperCache");

    protected readonly ContentPresenter ContentPresenter;
    protected readonly CompositeDisposable Subscriptions = new CompositeDisposable();
    protected readonly string ChildName;

    private FrameworkElement _Visual;
    public FrameworkElement Visual
    {
        get { return _Visual; }
        private set { this.SetProperty(ref _Visual, value); }
    }

    /// <summary>
    /// Creates a unique Cache key for a Combination of ContentPresenter + ChildName
    /// </summary>
    private static string CreateKey(ContentPresenter ContentPresenter, string ChildName)
    {
        var hash = 17;
        hash = hash * 23 + ContentPresenter.GetHashCode();

        if (ChildName != null)
            hash = hash * 23 + ChildName.GetHashCode();

        var result = hash.ToString();
        return result;
    }

    /// <summary>
    /// Creates an instance of ContentPresenterVisualHelper for the given ContentPresenter and ChildName, if necessary.
    /// Or returns an existing one from cache, if available.
    /// </summary>
    public static ContentPresenterVisualHelper GetInstance(ContentPresenter ContentPresenter, string ChildName)
    {
        string key = CreateKey(ContentPresenter, ChildName);
        var cachedObj = Cache.Get(key) as ContentPresenterVisualHelper;

        if (cachedObj != null)
            return cachedObj;

        lock (CacheLock)
        {
            cachedObj = Cache.Get(key) as ContentPresenterVisualHelper;

            if (cachedObj != null)
                return cachedObj;

            var obj = new ContentPresenterVisualHelper(ContentPresenter, ChildName);

            var cacheItem = new CacheItem(key, obj);
            var expiration = DateTimeOffset.Now + TimeSpan.FromSeconds(60);
            var policy = new CacheItemPolicy { AbsoluteExpiration = expiration };
            Cache.Set(cacheItem, policy);

            return obj;
        }
    }

    private ContentPresenterVisualHelper(ContentPresenter ContentPresenter, string ChildName)
    {
        this.ContentPresenter = ContentPresenter;
        this.ChildName = ChildName;

        this
            .ContentPresenter
            .ObserveDp(x => x.Content)  // extension method that creates an IObservable<object>, pushing values initially and then whenever the "ContentProperty"-dependency property changes
            .DistinctUntilChanged()
            .Subscribe(x => ContentPresenter_LayoutUpdated())
            .MakeDisposable(this.Subscriptions);  // extension method which just adds the IDisposable to this.Subscriptions

        /*
         * Alternative way? But probably not as good
         * 
        Observable.FromEventPattern(ContentPresenter, "LayoutUpdated")
            .Throttle(TimeSpan.FromMilliseconds(50))
            .Subscribe(x => ContentPresenter_LayoutUpdated())
            .MakeDisposable(this.Subscriptions);*/

    }

    public void Dispose()
    {
        this.Subscriptions.Dispose();
    }

    void ContentPresenter_LayoutUpdated()
    {
        Trace.WriteLine(String.Format("{0:hh.mm.ss:ffff} Content presenter updated: {1}", DateTime.Now, ContentPresenter.Content));

        if(!String.IsNullOrWhiteSpace(this.ChildName))
        {
            // Get Visual Child by name
            var child = this.ContentPresenter.FindChild<FrameworkElement>(this.ChildName);  // extension method, readily available on StackOverflow etc.
            this.Visual = child;
        }
        else
        {
            // Don't get child by name, but rather
            // Get the first - and only - immediate Visual Child of the ContentPresenter

            var N = VisualTreeHelper.GetChildrenCount(this.ContentPresenter);

            if (N == 0)
            {
                this.Visual = null;
                return;
            }

            if (N > 1)
                throw new InvalidOperationException("ContentPresenter had more than 1 Visual Children");

            var child = VisualTreeHelper.GetChild(this.ContentPresenter, 0);
            var _child = (FrameworkElement)child;

            this.Visual = _child;
        }
    }
}
链接地址: http://www.djcxy.com/p/71124.html

上一篇: 如何在服务器中启用.zip扩展

下一篇: 从外部绑定到ContentPresenter的视觉元素/子项