C# 绑定到ContentPresenter';s外部的视觉元素/儿童

C# 绑定到ContentPresenter';s外部的视觉元素/儿童,c#,wpf,xaml,data-binding,C#,Wpf,Xaml,Data Binding,首先是我的问题的简短“抽象”版本。 可能不需要讨论解决方案,但下面是我遇到的真正潜在问题的一些“可选”信息,只是为了理解上下文 所以:我有一个ContentPresenter,它使用DataTemplate生成绑定项的布局。 现在,在这个contentpresenter之外,我正在尝试在该contentpresenter中按名称绑定元素 假设以下伪XAML(MainTextBlock的绑定实际上不起作用): !!请假定MainTextblock的DataContext必须是(对)Conten

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

所以:我有一个ContentPresenter,它使用DataTemplate生成绑定项的布局。 现在,在这个contentpresenter之外,我正在尝试在该contentpresenter中按名称绑定元素

假设以下伪XAML(MainTextBlock的绑定实际上不起作用):


!!请假定MainTextblock的DataContext必须是(对)ContentPresenter的引用

基于这种假设,我如何使MainTextblock上的绑定工作

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

有没有办法做到这一点


现在我真的需要这个做什么?请不要读这篇文章,我相信不需要讨论上述问题的解决方案

我正在编写一个向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属性的类型为BindingBase(因此绑定是“延迟的”)。它旨在定义用于过滤的值。 假设要进行过滤时,每个过滤器循环遍历其绑定到的列的所有DataGridCell。对于每个DataGridCell,它将绑定的DataContext设置为相应的DataGridCell,并计算绑定

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

这个很好用

但是,我在尝试绑定到DataGridCell的可视内容时遇到了问题,例如第二个StringFilter的列为“{x:Reference SecondCol}”。 SecondCol是DataGridTemplateColumn。它的单元格内容将是ContentPresenter,其模板是DataGridTemplateColumn.CellTemplate,其内容是单元格包含的元素

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


谢谢

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

本质上,我为过滤器引入了第二个属性,称为“BindingContext”,也是BindingBase类型。使用者可以将其保留为null,在这种情况下,它将默认为相应的DataGridCell,或者它可以分配一个绑定(该绑定本身将获得DataContext=DataGridCell)。将评估此绑定,其结果将用作“binding”属性的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转换为包装类,包装类本身公开了一个“可视”属性。 根据用例的不同,此可视属性要么公开ContentPresenter的第一个也是唯一的直接可视子级,要么按名称查找可视子级。 我已经缓存了helper类的实例化,因为否则转换器会创建很多这样的类,并且每次它都会查询可视化树至少一次

它尝试将此属性与ContentPresenter保持同步;虽然我不认为有任何直接的方法可以监控它的可视树是否发生了变化,但每当ContentPresenter的内容属性发生变化时,我都会进行更新。(另一种方法可能是在布局发生变化时进行更新,但这显然在各种情况下都会触发,因此看起来有些过火)

[ValueConversion(typeof(ContentPresenter)、typeof(ContentPresenterVisualHelper))]
公共类ContentPresenterVisualHelperConverter:IValueConverter
{
/// 
///1.可以为null/空,在这种情况下,ContentPresenter的第一个可视子级由助手返回
///2.可以是字符串,在这种情况下,返回具有给定名称的ContentPresenter的子级
/// 
公共对象转换(对象值、类型targetType、对象参数、System.Globalization.CultureInfo区域性)
{
如果(值==null)
返回null;
ContentPresenter cp=作为ContentPresenter的值;
if(cp==null)
抛出新的InvalidOperationException(String.Format(“值必须是ContentPresenter类型,但为{0}”,value.GetType().FullName));
返回ContentPresenterVisualHelper.GetInstance(cp,参数为字符串);
}
public object ConvertBack(对象值,类型targetType,对象参数,System.Globalization.CultureInf
                <filter:StringFilter Column="{x:Reference SecondCol}"
                           BindingContext="{Binding Content, Converter={StaticResource ContentPresenterToVisualHelperConverter}, ConverterParameter='MyTextblock'}" 
                           Binding="{Binding Visual.Text, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"                                          
                         />
[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;
        }
    }
}