WPF C# UserControl DependencyProperty 命令绑定为空
WPF C# UserControl DependencyProperty Command Binding is null
我创建了一个 UserControl
作为文件浏览工具,我想将 Commands
实现为 DependencyProperties
用于加载和保存,以便我可以从我的 Commands
绑定ViewModel
以便处理它们。
现在的问题是,如果我使用像 Open
或 Save
这样的预定义 Commands
并在我的 Window
中处理它们,它会起作用,但如果我使用 Bindings
来自我的 ViewModel
这些 Commands
是 null
...
以下代码是一个示例程序,我在其中删除了对我的问题不重要的所有内容,因为它的代码太多了。
用户控件
XAML
<UserControl x:Class="WpfApplication1.TestControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:models="clr-namespace:WpfApplication1.Models"
Height="21" Width="80" Margin="2">
<UserControl.Resources>
<models:UserControlViewModel x:Key="ViewModel"/>
</UserControl.Resources>
<UserControl.DataContext>
<Binding Source="{StaticResource ViewModel}"/>
</UserControl.DataContext>
<Grid>
<Button Content="_Load" IsDefault="True"
Command="{Binding Path=ExecuteCommand, Source={StaticResource ViewModel}}"
CommandParameter="{Binding Path=LoadCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType ={x:Type local:TestControl}}}"/>
</Grid>
代码隐藏
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace WpfApplication1
{
public partial class TestControl : UserControl
{
public static readonly DependencyProperty LoadCommandProperty = DependencyProperty.Register(nameof(LoadCommand), typeof(ICommand), typeof(TestControl), new PropertyMetadata(null));
public ICommand LoadCommand
{
get { return (ICommand)GetValue(LoadCommandProperty); }
set { SetValue(LoadCommandProperty, value); }
}
public TestControl()
{
InitializeComponent();
}
}
}
视图模型
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using Prism.Commands;
namespace WpfApplication1.Models
{
public class UserControlViewModel
: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public DelegateCommand<ICommand> ExecuteCommand { get; }
private void ExecuteCommand_Executed(ICommand cmd) => cmd?.Execute("C:\Test.txt");
private void Notify([CallerMemberName] string name = null)
{
if (name != null)
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public UserControlViewModel()
{
ExecuteCommand = new DelegateCommand<ICommand>(ExecuteCommand_Executed);
}
}
}
Window
XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:models="clr-namespace:WpfApplication1.Models"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="200">
<Window.Resources>
<models:MainWindowViewModel x:Key="ViewModel"/>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource ViewModel}"/>
</Window.DataContext>
<Window.CommandBindings>
<CommandBinding Command="Open" Executed="CommandBinding_Executed"/>
</Window.CommandBindings>
<StackPanel VerticalAlignment="Center">
<local:TestControl LoadCommand="{Binding Path=OpenCommand, Source={StaticResource ViewModel}}"/>
<local:TestControl LoadCommand="Open"/>
</StackPanel>
</Window>
代码隐藏
using System.Windows;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void CommandBinding_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
MessageBox.Show($"Window: {e.Parameter.ToString()}");
}
}
}
视图模型
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using Prism.Commands;
namespace WpfApplication1.Models
{
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public DelegateCommand<string> OpenCommand { get; }
private void OpenCommand_Executed(string file)
{
MessageBox.Show($"Model: {file}");
}
private void Notify([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public MainWindowViewModel()
{
OpenCommand = new DelegateCommand<string>(OpenCommand_Executed);
}
}
}
window 包含一个预定义的 Open Command
,这种方式有效,但 Binding
无效。
为了 运行 这个应用程序,你需要 Prism.Wpf NuGet 包。
看来你从这里的深处钓到了什么奇怪的东西。但是用我的一个朋友的话来说,"The good news is, the cancer is easier to treat this time"。
首先我使用 PresentationTraceSources.TraceLevel
:
对你的绑定进行了活检
<local:TestControl
LoadCommand="{Binding
Source={StaticResource ViewModel},
Path=OpenCommand,
PresentationTraceSources.TraceLevel=High}"
/>
这是我得到的:
System.Windows.Data Warning: 56 : Created BindingExpression (hash=34810426) for Binding (hash=11882558)
System.Windows.Data Warning: 58 : Path: 'OpenCommand'
System.Windows.Data Warning: 60 : BindingExpression (hash=34810426): Default mode resolved to OneWay
System.Windows.Data Warning: 61 : BindingExpression (hash=34810426): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 62 : BindingExpression (hash=34810426): Attach to WpfApplication1.TestControl.LoadCommand (hash=5114324)
System.Windows.Data Warning: 67 : BindingExpression (hash=34810426): Resolving source
System.Windows.Data Warning: 70 : BindingExpression (hash=34810426): Found data context element: <null> (OK)
System.Windows.Data Warning: 78 : BindingExpression (hash=34810426): Activate with root item UserControlViewModel (hash=33108977)
System.Windows.Data Warning: 108 : BindingExpression (hash=34810426): At level 0 - for UserControlViewModel.OpenCommand found accessor <null>
System.Windows.Data Error: 40 : BindingExpression path error: 'OpenCommand' property not found on 'object' ''UserControlViewModel' (HashCode=33108977)'. BindingExpression:Path=OpenCommand; DataItem='UserControlViewModel' (HashCode=33108977); target element is 'TestControl' (Name=''); target property is 'LoadCommand' (type 'ICommand')
System.Windows.Data Warning: 80 : BindingExpression (hash=34810426): TransferValue - got raw value {DependencyProperty.UnsetValue}
System.Windows.Data Warning: 88 : BindingExpression (hash=34810426): TransferValue - using fallback/default value <null>
System.Windows.Data Warning: 89 : BindingExpression (hash=34810426): TransferValue - using final value <null>
情况如下:在 MainWindow.xaml
中,在 TestControl
的上下文中查找 {StaticResource ViewModel}
。效果等同于在TestControl
中调用FindResource
:
public TestControl()
{
InitializeComponent();
var x = FindResource("ViewModel");
// Set breakpoint here and inspect x
;
}
TestControl
有自己的资源,键为 ViewModel
,所以这就是查找找到的内容。该资源是 UserControlViewModel
,它没有 OpenCommand
属性,在这种情况下,它 隐藏 完全不同的同名资源 MainWindow
。
我不知道你从哪里得到这个视图模型资源方案,但你可以看到这是一个严重的错误。 MainWindow
中的任何人都不应该担心 TestControl
中内部使用的资源密钥。
幸运的是,没有必要制造那个问题。您可以删除一大块代码,最终得到更简单、更健壮且更易于维护的代码。
因此,解决问题:
首先,不要将所有视图模型创建为资源,因为没有理由这样做并且会导致问题。将此 ExecuteCommand
绑定与您拥有的绑定进行比较。你从这些东西中得到了什么?没有。如果您费心设置 DataContext
, 将其用作 DataContext。
<UserControl.DataContext>
<models:UserControlViewModel />
</UserControl.DataContext>
<Grid>
<Button Content="_Load" IsDefault="True"
Command="{Binding ExecuteCommand}"
CommandParameter="{Binding Path=LoadCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TestControl}}}"/>
</Grid>
下面是 MainWindow
的样子(省略了您不想要的 CommandBindings
):
<Window.DataContext>
<models:MainWindowViewModel />
</Window.DataContext>
<StackPanel VerticalAlignment="Center">
<!--
local:TestControl.DataContext is its own viewmodel, so we use RelativeSource
to get to the Window, and then we look at Window.DataContext for the main window
viewmodel.
-->
<local:TestControl
LoadCommand="{Binding DataContext.OpenCommand, RelativeSource={RelativeSource AncestorType=Window}}"
/>
</StackPanel>
最后,对于用户控件来说,创建自己的视图模型通常是不好的做法,原因现在已经很明显了。我发现当他们继承 parent 的 DataContext
时,混淆会少得多。但我已经将您的设计从 window 中扔掉了一天,所以我们将不理会它。
我创建了一个 UserControl
作为文件浏览工具,我想将 Commands
实现为 DependencyProperties
用于加载和保存,以便我可以从我的 Commands
绑定ViewModel
以便处理它们。
现在的问题是,如果我使用像 Open
或 Save
这样的预定义 Commands
并在我的 Window
中处理它们,它会起作用,但如果我使用 Bindings
来自我的 ViewModel
这些 Commands
是 null
...
以下代码是一个示例程序,我在其中删除了对我的问题不重要的所有内容,因为它的代码太多了。
用户控件
XAML
<UserControl x:Class="WpfApplication1.TestControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:models="clr-namespace:WpfApplication1.Models"
Height="21" Width="80" Margin="2">
<UserControl.Resources>
<models:UserControlViewModel x:Key="ViewModel"/>
</UserControl.Resources>
<UserControl.DataContext>
<Binding Source="{StaticResource ViewModel}"/>
</UserControl.DataContext>
<Grid>
<Button Content="_Load" IsDefault="True"
Command="{Binding Path=ExecuteCommand, Source={StaticResource ViewModel}}"
CommandParameter="{Binding Path=LoadCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType ={x:Type local:TestControl}}}"/>
</Grid>
代码隐藏
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace WpfApplication1
{
public partial class TestControl : UserControl
{
public static readonly DependencyProperty LoadCommandProperty = DependencyProperty.Register(nameof(LoadCommand), typeof(ICommand), typeof(TestControl), new PropertyMetadata(null));
public ICommand LoadCommand
{
get { return (ICommand)GetValue(LoadCommandProperty); }
set { SetValue(LoadCommandProperty, value); }
}
public TestControl()
{
InitializeComponent();
}
}
}
视图模型
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using Prism.Commands;
namespace WpfApplication1.Models
{
public class UserControlViewModel
: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public DelegateCommand<ICommand> ExecuteCommand { get; }
private void ExecuteCommand_Executed(ICommand cmd) => cmd?.Execute("C:\Test.txt");
private void Notify([CallerMemberName] string name = null)
{
if (name != null)
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public UserControlViewModel()
{
ExecuteCommand = new DelegateCommand<ICommand>(ExecuteCommand_Executed);
}
}
}
Window
XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:models="clr-namespace:WpfApplication1.Models"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="200">
<Window.Resources>
<models:MainWindowViewModel x:Key="ViewModel"/>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource ViewModel}"/>
</Window.DataContext>
<Window.CommandBindings>
<CommandBinding Command="Open" Executed="CommandBinding_Executed"/>
</Window.CommandBindings>
<StackPanel VerticalAlignment="Center">
<local:TestControl LoadCommand="{Binding Path=OpenCommand, Source={StaticResource ViewModel}}"/>
<local:TestControl LoadCommand="Open"/>
</StackPanel>
</Window>
代码隐藏
using System.Windows;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void CommandBinding_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
MessageBox.Show($"Window: {e.Parameter.ToString()}");
}
}
}
视图模型
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using Prism.Commands;
namespace WpfApplication1.Models
{
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public DelegateCommand<string> OpenCommand { get; }
private void OpenCommand_Executed(string file)
{
MessageBox.Show($"Model: {file}");
}
private void Notify([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public MainWindowViewModel()
{
OpenCommand = new DelegateCommand<string>(OpenCommand_Executed);
}
}
}
window 包含一个预定义的 Open Command
,这种方式有效,但 Binding
无效。
为了 运行 这个应用程序,你需要 Prism.Wpf NuGet 包。
看来你从这里的深处钓到了什么奇怪的东西。但是用我的一个朋友的话来说,"The good news is, the cancer is easier to treat this time"。
首先我使用 PresentationTraceSources.TraceLevel
:
<local:TestControl
LoadCommand="{Binding
Source={StaticResource ViewModel},
Path=OpenCommand,
PresentationTraceSources.TraceLevel=High}"
/>
这是我得到的:
System.Windows.Data Warning: 56 : Created BindingExpression (hash=34810426) for Binding (hash=11882558)
System.Windows.Data Warning: 58 : Path: 'OpenCommand'
System.Windows.Data Warning: 60 : BindingExpression (hash=34810426): Default mode resolved to OneWay
System.Windows.Data Warning: 61 : BindingExpression (hash=34810426): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 62 : BindingExpression (hash=34810426): Attach to WpfApplication1.TestControl.LoadCommand (hash=5114324)
System.Windows.Data Warning: 67 : BindingExpression (hash=34810426): Resolving source
System.Windows.Data Warning: 70 : BindingExpression (hash=34810426): Found data context element: <null> (OK)
System.Windows.Data Warning: 78 : BindingExpression (hash=34810426): Activate with root item UserControlViewModel (hash=33108977)
System.Windows.Data Warning: 108 : BindingExpression (hash=34810426): At level 0 - for UserControlViewModel.OpenCommand found accessor <null>
System.Windows.Data Error: 40 : BindingExpression path error: 'OpenCommand' property not found on 'object' ''UserControlViewModel' (HashCode=33108977)'. BindingExpression:Path=OpenCommand; DataItem='UserControlViewModel' (HashCode=33108977); target element is 'TestControl' (Name=''); target property is 'LoadCommand' (type 'ICommand')
System.Windows.Data Warning: 80 : BindingExpression (hash=34810426): TransferValue - got raw value {DependencyProperty.UnsetValue}
System.Windows.Data Warning: 88 : BindingExpression (hash=34810426): TransferValue - using fallback/default value <null>
System.Windows.Data Warning: 89 : BindingExpression (hash=34810426): TransferValue - using final value <null>
情况如下:在 MainWindow.xaml
中,在 TestControl
的上下文中查找 {StaticResource ViewModel}
。效果等同于在TestControl
中调用FindResource
:
public TestControl()
{
InitializeComponent();
var x = FindResource("ViewModel");
// Set breakpoint here and inspect x
;
}
TestControl
有自己的资源,键为 ViewModel
,所以这就是查找找到的内容。该资源是 UserControlViewModel
,它没有 OpenCommand
属性,在这种情况下,它 隐藏 完全不同的同名资源 MainWindow
。
我不知道你从哪里得到这个视图模型资源方案,但你可以看到这是一个严重的错误。 MainWindow
中的任何人都不应该担心 TestControl
中内部使用的资源密钥。
幸运的是,没有必要制造那个问题。您可以删除一大块代码,最终得到更简单、更健壮且更易于维护的代码。
因此,解决问题:
首先,不要将所有视图模型创建为资源,因为没有理由这样做并且会导致问题。将此 ExecuteCommand
绑定与您拥有的绑定进行比较。你从这些东西中得到了什么?没有。如果您费心设置 DataContext
, 将其用作 DataContext。
<UserControl.DataContext>
<models:UserControlViewModel />
</UserControl.DataContext>
<Grid>
<Button Content="_Load" IsDefault="True"
Command="{Binding ExecuteCommand}"
CommandParameter="{Binding Path=LoadCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TestControl}}}"/>
</Grid>
下面是 MainWindow
的样子(省略了您不想要的 CommandBindings
):
<Window.DataContext>
<models:MainWindowViewModel />
</Window.DataContext>
<StackPanel VerticalAlignment="Center">
<!--
local:TestControl.DataContext is its own viewmodel, so we use RelativeSource
to get to the Window, and then we look at Window.DataContext for the main window
viewmodel.
-->
<local:TestControl
LoadCommand="{Binding DataContext.OpenCommand, RelativeSource={RelativeSource AncestorType=Window}}"
/>
</StackPanel>
最后,对于用户控件来说,创建自己的视图模型通常是不好的做法,原因现在已经很明显了。我发现当他们继承 parent 的 DataContext
时,混淆会少得多。但我已经将您的设计从 window 中扔掉了一天,所以我们将不理会它。