WPF 的 ElementName 在 ContextMenu 中無法繫結成功?試試使用 x:Reference!
在 Binding 中使用 ElementName 司空見慣,沒見它出過什麼事兒。不過當你預見 ContextMenu,或者類似 Grid.Row / Grid.Column 這樣的屬性中設定的時候,ElementName 就不那麼管用了。
本文將解決這個問題。
以下程式碼是可以正常工作的
<Window x:Class="Walterlv.Demo.BindingContext.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="WalterlvWindow" Title="Walterlv Binding Demo" Height="450" Width="800"> <Grid Background="LightGray" Margin="1 1 1 0" MinHeight="40"> <TextBlock> <Run Text="{Binding Mode=OneWay}" FontSize="20" /> <LineBreak /> <Run Text="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </TextBlock> </Grid> </Window>
在程式碼中,我們為一段文字中的一個部分綁定了主視窗的的一個屬性,於是我們使用 ElementName
來指定繫結源為 WalterlvWindow
。
▲ 使用普通的 ElementName 繫結
以下程式碼就無法正常工作了
保持以上程式碼不變,我們現在新增一個 ContextMenu
,然後在 ContextMenu
中使用一模一樣的繫結表示式:
<Window x:Class="Walterlv.Demo.BindingContext.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="WalterlvWindow" Title="Walterlv Binding Demo" Height="450" Width="800"> <Grid Background="LightGray" Margin="1 1 1 0" MinHeight="40"> <Grid.ContextMenu> <ContextMenu> <MenuItem Header="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </ContextMenu> </Grid.ContextMenu> <TextBlock> <Run Text="{Binding Mode=OneWay}" FontSize="20" /> <LineBreak /> <Run Text="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </TextBlock> </Grid> </Window>
注意, MenuItem
的 Header
屬性設定為和 Run
的 Text
屬性一模一樣的繫結字串。不過執行之後的截圖顯示,右鍵選單中並沒有如預期般出現繫結的字串。
使用 x:Reference 代替 ElementName 能夠解決
以上繫結失敗的原因,是 Grid.ContextMenu
屬性中賦值的 ContextMenu
不在視覺化樹中,而 ContextMenu
又不是一個預設建立 ScopeName 的控制元件,此時既沒有自己指定 NameScope,有沒有通過視覺化樹尋找上層設定的 NameScope,所以在繫結上下文中是找不到 WalterlvWindow
的。如果呼叫去查詢,得到的是 null
。詳見:WPF 中的 NameScope。
類似的情況也發生在設定非視覺化樹或邏輯樹的屬性時,典型的比如在 Grid.Row
或 Grid.Column
屬性上繫結時, ElementName
也是失效的。
此時最適合的情況是直接使用 x:Reference
。
<Window x:Class="Walterlv.Demo.BindingContext.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="WalterlvWindow" Title="Walterlv Binding Demo" Height="450" Width="800"> <Grid Background="LightGray" Margin="1 1 1 0" MinHeight="40"> <Grid.ContextMenu> <ContextMenu> -<MenuItem Header="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> +<MenuItem Header="{Binding Source={x:Reference WalterlvWindow}, Path=DemoText, Mode=OneWay}" /> </ContextMenu> </Grid.ContextMenu> <TextBlock> <Run Text="{Binding Mode=OneWay}" FontSize="20" /> <LineBreak /> <Run Text="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </TextBlock> </Grid> </Window>
不過,這是個假象,因為此程式碼執行時會丟擲異常:
XamlObjectWriterException: Cannot call MarkupExtension.ProvideValue because of a cyclical dependency. Properties inside a MarkupExtension cannot reference objects that reference the result of the MarkupExtension. The affected MarkupExtensions are:‘System.Windows.Data.Binding’ Line number ‘8’ and line position ‘27’.
因為給 MenuItem
的 Header
屬性繫結賦值的時候,建立繫結表示式用到了 WalterlvWindow
,但此時 WalterlvWindow
尚在構建(因為裡面的 ContextMenu
是視窗的一部分),於是出現了迴圈依賴。而這是不允許的。
為了解決迴圈依賴問題,我們可以考慮將 x:Reference
放到資源中。因為資源是按需建立的,所以這不會造成迴圈依賴。
那麼總得有一個物件來承載我們的繫結源。拿控制元件的 Tag
屬性也許是一個方案,不過專門為此建立一個繫結代理類也許是一個更符合語義的方法:
<Window x:Class="Walterlv.Demo.BindingContext.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" +xmlns:local="clr-namespace:Walterlv.Demo.BindingContext" x:Name="WalterlvWindow" Title="Walterlv Binding Demo" Height="450" Width="800"> +<Window.Resources> +<local:BindingProxy x:Key="WalterlvBindingProxy" Data="{x:Reference WalterlvWindow}" /> +</Window.Resources> <Grid Background="LightGray" Margin="1 1 1 0" MinHeight="40"> <Grid.ContextMenu> <ContextMenu> -<MenuItem Header="{Binding Source={x:Reference WalterlvWindow}, Path=DemoText, Mode=OneWay}" /> +<MenuItem Header="{Binding Source={StaticResource WalterlvBindingProxy}, Path=Data.DemoText, Mode=OneWay}" /> </ContextMenu> </Grid.ContextMenu> <TextBlock> <Run Text="{Binding Mode=OneWay}" FontSize="20" /> <LineBreak /> <Run Text="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </TextBlock> </Grid> </Window>
至於 BindingProxy
,非常簡單:
public sealed class BindingProxy : Freezable { public static readonly DependencyProperty DataProperty = DependencyProperty.Register( "Data", typeof(object), typeof(BindingProxy), new PropertyMetadata(default(object))); public object Data { get => (object) GetValue(DataProperty); set => SetValue(DataProperty, value); } protected override Freezable CreateInstanceCore() => new BindingProxy(); public override string ToString() => Data is FrameworkElement fe ? $"BindingProxy: {fe.Name}" : $"Binding Proxy: {Data?.GetType().FullName}"; }
現在執行,右鍵選單已經正常完成了繫結。
▲ 右鍵選單已經正常完成了繫結
參考資料
- ofollow,noindex" target="_blank">c# - WPF databinding error in Tag property - Stack Overflow
本文會經常更新,請閱讀原文: https://walterlv.com/post/fix-wpf-binding-issues-in-context-menu.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名 呂毅 (包含連結:https://walterlv.com ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請 與我聯絡 ([email protected]) 。