WPF C# UserControl DependencyProperty 命令绑定为空

WPF C# UserControl DependencyProperty Command Binding is null

我创建了一个 UserControl 作为文件浏览工具,我想将 Commands 实现为 DependencyProperties 用于加载和保存,以便我可以从我的 Commands 绑定ViewModel 以便处理它们。

现在的问题是,如果我使用像 OpenSave 这样的预定义 Commands 并在我的 Window 中处理它们,它会起作用,但如果我使用 Bindings来自我的 ViewModel 这些 Commandsnull...

以下代码是一个示例程序,我在其中删除了对我的问题不重要的所有内容,因为它的代码太多了。


用户控件

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 中扔掉了一天,所以我们将不理会它。