从外部绑定到ContentPresenter的可视元素/子元素

时间:2016-06-29 09:48:09

标签: c# wpf xaml data-binding

首先是我的问题的简短“抽象”简短版本。 可能不需要讨论一个解决方案,但是在我正在解决的真正潜在问题的某些“可选”信息之下,只是为了理解上下文。

所以:我有一个使用DataTemplate的ContentPresenter来为绑定项生成其布局。 现在,在这个内容展示者之外,我正试图在该内容展示者中按名称绑定元素。

假设以下Pseudo-XAML(MainTextBlock的绑定在实践中不起作用):

   <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>

!!请假设MainTextblock的DataContext必须(引用)TheContentPresenter !!

鉴于这种假设,我如何才能使MainTextblock上的绑定工作?

我无法绑定到ContentPresenter的Content属性,因为它包含绑定元素(例如SomeItem),而不是其可视化表示。 不幸的是,ContentPresenter似乎没有任何代表其Visual Tree / Visual Children的属性。

有没有办法做到这一点?

现在我真的需要这个吗?随意跳过阅读本文,不应该讨论我认为的上述问题的解决方案。

我正在编写一个向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”包含每列的各个过滤器,例如其中第一个是一个过滤器,允许在其绑定的任何列中搜索文本(在本例中为FirstCol),并隐藏不显示该文本的列。

现在,Binding是一个有趣的部分。 Binding属性的类型为BindingBase(因此绑定是“延迟”)。它旨在定义用于过滤的值。 当应该发生过滤时,每个过滤器循环遍历其绑定的列的所有DataGridCell。对于每个DataGridCell,它将Binding的DataContext设置为相应的DataGridCell,并评估绑定。

因此,StringFilter将循环遍历FirstCol中的每个DataGridCell。 对于它们中的每一个,它将检索BindingBase“Binding”(即{Binding DataContext.Value1}),将其DataContext设置为DataGridCell,并对其进行评估。 因此,在这种情况下,它将绑定到WpfGridCell.DataContext.Value1,或者换句话说绑定到DataGridCell包含的项的Value1属性。 稍后,它将检查这些评估的项是否与用户输入的用于过滤的字符串匹配。

这很好用。

但是,我在尝试绑定到DataGridCell的可视内容时遇到了麻烦,就像第二个StringFilter with Column =“{x:Reference SecondCol}”的情况一样。 SecondCol是一个DataGridTemplateColumn。它的单元格内容将是一个ContentPresenter,其模板是DataGridTemplateColumn.CellTemplate,其内容是单元格包含的元素。

这是我们从上面回到简化版本的地方。我现在需要用DataContext = DataGridCell来评估“绑定”,并以某种方式提出一个绑定,让我绑定到DataGridCell.Content中给出的ContentPresenter的可视元素。

谢谢!

1 个答案:

答案 0 :(得分:0)

由于到目前为止没有其他解决方案出现/仅使用XAML似乎不可能,这是我目前的解决方案。看起来有点混乱,但它可以工作并允许相对一般的使用。

基本上,我在我的过滤器中引入了第二个属性,名为“BindingContext”,也是BindingBase类型。消费者可以将其保留为null,在这种情况下,它将默认为相应的DataGridCell,或者它可以分配绑定(它本身将获得DataContext = DataGridCell)。将评估此绑定,其结果将用作“绑定”属性的datacontext:

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

现在我已经创建了一个IValueConverter,它将ContentPresenter转换为包装类,它本身公开了一个“Visual”属性。 根据用例,此可视属性要么暴露ContentPresenters的第一个也是唯一的直接可视子对象,要么按名称查找可视子对象。 我已经缓存了辅助类的实例化,因为否则转换器会创建相当多的这些,并且每次它至少查询一次Visual Tree。

它尝试将此属性与ContentPresenter保持同步;虽然我没有任何直接的方法来监视它的Visual Tree是否发生了变化,但只要ContentPresenter的Content属性发生变化,我就会进行更新。 (另一种方式可能是在布局发生变化时进行更新,但在各种情况下显然会触发很多情况,所以看起来有点矫枉过正)

[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;
        }
    }
}