首先是我的问题的简短“抽象”简短版本。 可能不需要讨论一个解决方案,但是在我正在解决的真正潜在问题的某些“可选”信息之下,只是为了理解上下文。
所以:我有一个使用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的可视元素。
谢谢!
答案 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;
}
}
}