从外部绑定到ContentPresenter的视觉元素/子项
首先是一个简短的“抽象”简短版本的问题。 可能不需要讨论一个解决方案,但是下面有一些我选择的真正基础问题的进一步“可选”信息,只是为了理解上下文。
所以:我有一个ContentPresenter使用DataTemplate为绑定项目生成其布局。 现在,除了这个contentpresenter之外,我试图在该内容展示者的名称内通过名称进行绑定。
假设下面的伪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似乎没有任何代表其可视化树/视觉儿童的属性。
有没有办法做到这一点?
现在我真的需要这样做吗? 随意跳过阅读本文,不需要讨论我相信上述问题的解决方案。
我正在编写一个将可定制过滤器添加到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(所以绑定是“延迟”)。 它旨在定义用于过滤的值。 当应该进行过滤时,每个过滤器都将循环遍历与其绑定的列的所有DataGridCells。 对于每个DataGridCell,它将Binding的DataContext设置为相应的DataGridCell,并评估绑定。
所以,StringFilter会遍历FirstCol中的每个DataGridCell。 对于他们每个人来说,它会检索BindingBase“Binding”(即{Binding DataContext.Value1}),将其DataContext设置为DataGridCell,并对其进行评估。 所以在这种情况下,它会绑定到WpfGridCell.DataContext.Value1,或换言之,绑定到DataGridCell包含的项目的Value1属性。 稍后,它将检查这些评估项是否与用户为了过滤而输入的字符串相匹配。
这工作得很好。
但是,在尝试绑定到DataGridCell的可视内容时遇到了问题,例如Column =“{x:Reference SecondCol}”中的第二个StringFilter。 SecondCol是一个DataGridTemplateColumn。 它的单元格内容将是ContentPresenter,其模板是DataGridTemplateColumn.CellTemplate,其内容是单元格包含的元素。
这是我们从上面回到我的简化版本的地方。 我现在需要用DataContext = DataGridCell来评估“绑定”,并以某种方式想出一个绑定,让我将它绑定到DataGridCell.Content中给出的ContentPresenter的可视元素。
谢谢!
由于到目前为止还没有其他解决方案出现/这看起来不适用于XAML,这是我目前的解决方案。 似乎有点混乱,但它起作用并允许相对普遍的使用。
本质上,我已经将第二个属性引入了名为“BindingContext”的过滤器,也是BindingBase类型的过滤器。 消费者可以将它保留为空,在这种情况下,它将默认为相应的DataGridCell,或者它可以分配一个绑定(它本身将获得DataContext = DataGridCell)。 该绑定将被评估,并且其结果将被用作“绑定”属性的数据上下文:
<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的第一个也是唯一的直接视觉子视图,或者按名称查找视觉子视图。 我已经缓存了helper类的实例化,否则转换器会创建相当多的这些,并且每次它至少会查询一次Visual Tree。
它试图保持此属性同步到ContentPresenter; 虽然我不觉得有任何直接的方法来监视它的可视化树是否发生变化,但每当ContentPresenter的内容属性发生变化时,我都会进行更新。 (另一种方式可能是更新它的布局变化时,但这显然会在各种情况下触发很多,所以看起来像矫枉过正)
[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/71123.html
上一篇: Binding to a ContentPresenter's visual elements/children from outside
下一篇: How can I change a wpf Databound grid cell based on the cell value?