绑定时DataItem为空,找不到原因?

6

我试图重现在Sheridan的答案中所建议的方法在使用WPF和MVVM模式时浏览视图。不幸的是,当我这样做时,我遇到了绑定错误。以下是确切的错误:

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='JollyFinance.ViewModels.MainViewModel', AncestorLevel='1''. BindingExpression:Path=DataContext.DisplayTest; DataItem=null; target element is 'Button' (Name=''); target property is 'Command' (type 'ICommand')

当我查看LoginView.xaml中的xaml代码时,我注意到Visual Studio告诉我在MainViewModel类型的上下文中找不到DataContext.DisplayText。我已经尝试删除DataContext.仅保留DisplayText,但没有效果。除非Sheridan的答案有错误,否则我肯定是漏掉了什么。为使其正常工作,我该怎么做?
MainWindow.xaml:
<Window x:Class="JollyFinance.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:JollyFinance.ViewModels"
        xmlns:views="clr-namespace:JollyFinance.Views"
        Title="JollyFinance!" Height="720" Width="1280">

    <Window.Resources>
        <!-- Different pages -->
        <DataTemplate DataType="{x:Type vm:LoginViewModel}">
            <views:LoginView/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:TestViewModel}">
            <views:Test/>
        </DataTemplate>
    </Window.Resources>

    <Window.DataContext>
        <vm:MainViewModel/>
    </Window.DataContext>

    <Grid>
        <ContentControl Content="{Binding CurrentViewModel}"/>
    </Grid>
</Window>

MainViewModel.cs:

public class MainViewModel : BindableObject
{
    private ViewModelNavigationBase _currentViewModel;

    public MainViewModel()
    {
        CurrentViewModel = new LoginViewModel();
    }

    public ICommand DisplayTest
    {
        get
        {
            // This is added just to see if the ICommand is actually called when I press the
            // Create New User button
            Window popup = new Window();
            popup.ShowDialog();

            // View model that doesn't contain anything for now
            return new RelayCommand(action => CurrentViewModel = new TestViewModel());
        }
    }

    public ViewModelNavigationBase CurrentViewModel
    {
        get { return _currentViewModel; }
        set
        {
            if (_currentViewModel != value)
            {
                _currentViewModel = value;
                RaisePropertyChanged("CurrentViewModel");
            }
        }
    }
}

LoginView.xaml:

<UserControl x:Class="JollyFinance.Views.LoginView"
             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:vm="clr-namespace:JollyFinance.ViewModels"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.DataContext>
        <vm:LoginViewModel/>
    </UserControl.DataContext>

    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <TextBlock Text="Username: " Grid.Column="1" Grid.Row="1" Margin="5"/>
        <TextBox Text="{Binding Path=Username}" Grid.Column="2" Grid.Row="1" Grid.ColumnSpan="2" Margin="5"/>

        <TextBlock Text="Password: " Grid.Column="1" Grid.Row="2" Margin="5"/>
        <PasswordBox x:Name="PasswordBox" PasswordChar="*" Grid.Column="2" Grid.ColumnSpan="2" Grid.Row="2" Margin="5"/>

        <Button Content="Log In" Grid.Column="2" Grid.Row="3" Margin="5" Padding="5" Command="{Binding LoginCommand}"/>
        <Button Content="Create new user" Grid.Column="3" Grid.Row="3" Margin="5" Padding="5" 
                Command="{Binding DataContext.DisplayTest, RelativeSource={RelativeSource AncestorType={x:Type vm:MainViewModel}}, 
            Mode=OneWay}"/>

    </Grid>

</UserControl>

LoginViewModel.cs:

public class LoginViewModel : ViewModelNavigationBase
{
    public LoginViewModel()
    {
        LoginCommand = new RelayCommand(Login);
    }

    private void Login(object param)
    {
        // Just there to make sure the ICommand is actually called when I press the
        // Login button             
        Window popup = new Window();
        popup.ShowDialog();
    }

    public String Username { get; set; }

    public String Password { get; set; }

    public ICommand LoginCommand { get; set; }
}

ViewModelNavigationBase 是一个实现了 INotifyPropertyChanged 接口的类,而 Test.xaml 和 TestViewModel.cs 只是用于测试目的的虚拟视图模型/视图。

3个回答

1
在我的回答中,我提到你应该在App.xaml中声明你的视图模型DataTemplate,这样每个视图都可以访问它们。把它们放在MainWindow类中是你的第一个问题。
另一个错误是你的ICommand的Binding Path。如果你想要从被设置为Window.DataContext的视图模型中访问某些内容,那么你不应该使用RelativeSource Binding。相反,尝试使用以下方法:
<Button Content="Create new user" Grid.Column="3" Grid.Row="3" Margin="5" Padding="5" 
    Command="{Binding DataContext.DisplayTest}, Mode=OneWay}" />

请记住,无论出于什么原因,您选择不将MainViewModel类扩展为ViewModelNavigationBase类...这也可能会导致问题。
无论如何,如果这不能解决您的问题,请告诉我。另外,如果您想在任何时候通知Stack Overflow上的用户,只需在他们的名称前面加上@符号,他们就会收到通知。如果您这样做,您可以直接向我提出这个问题。

1

MainViewModel不是可视或逻辑树中的直接祖先,这就是为什么RelativeSource={RelativeSource AncestorType={x:Type vm:MainViewModel}}无法找到它的原因。

如何解决?首先,请不要尝试通过各种UI组件来触发命令。仅仅因为你在互联网上看到过类似的做法并不意味着它是一个理想的设计选择。这样做意味着LoginView深刻了解其他视图和视图模型——这是不好的。如果你要这样做,那么你可能需要将所有东西都编码为一个单一的UI类,其中包含一个真正的只是大量代码的代码后台类。

一个更好(但仍不是最佳)的方法是让MainView(或viewmodel)生成LoginView。因为它持有对视图的引用,所以它也负责处理它。因此,LoginView可以显示以收集凭据,然后主视图可以在信号验证成功时处理它。或者它只是收集凭据,让MainView/viewmodel来验证它们(可以通过MainView/viewmodel触发后台调用来检查凭据是否与存储相匹配来完成)。

一个简单(粗略的)经验法则是:父视图可以了解子视图,但一般来说反过来不应该发生。 MVVM 是关于解耦和分离功能,但实际上你却紧密的耦合它们。当然,所有这些都比我所示范的要复杂得多,但您仍然可以在保持实用性和不过度工程化的情况下执行一些操作。
因此,TLDR;:
  • LoginView(或其视图模型)应该实现自己的命令来处理按钮点击
  • 不要深入到另一个视图的内部 来触发功能
  • 追求SRP和解耦代码/视图
  • 在使用祖先绑定时,寻找在视觉/逻辑树中的元素

已经发生的情况是,我的 LoginViewModel(和视图)在创建时由我的 MainViewModel 生成。但我的 LoginViewModel 或视图如何向 MainViewModel 发信号表明凭据已经验证过了呢?这就是为什么我使用链接问题答案中的方法。此外,您提到我所做的事情很糟糕,因为它将所有东西都耦合在一起。除了使用 Page 之外,还有什么其他方法可以导航浏览视图呢?这就是我最初寻找链接问题答案之前想要的。 - Choub890
1
在MVVM风格中,ViewModels使用直接事件处理(如果一个VM创建另一个VM)或使用Messenger类(如果VM是独立的)相互交互。 Messenger具有公共事件,在方法Messenger.Send(eventArgs)中触发。每个视图模型都为Messenger的公共事件注册事件处理程序。请查看MVVM Light工具包。 - opewix
1
使用 Frame 控件来在页面之间进行导航。将一个 Frame 放置在 MainWindow 中,保存对该实例的引用,并使用 Frame.NavigateTo 方法进行导航。 - opewix
谢谢你们迄今为止的回答。你能指点我一个关于如何使用事件处理程序进行导航的好教程吗?我已经搜索了很多,所有似乎都弹出了与“mvvm视图导航”相关的方法,这是与此问题链接的答案中描述的方法...这是我认为它是这样做的另一个原因。再次感谢。 - Choub890
https://dev59.com/xGQn5IYBdhLWcg3wPlCj - opewix
@Choub890,您可以使用布尔属性(保持简单)或对话框结果(稍微复杂一些)来实现。像Jesse所提到的那样,直接处理事件是一个选项,但消息传递更适用于完全断开连接的对象,而不是具有父子关系的对象。 - slugster

0

在应用作用域中将 MainViewModel 定义为静态资源。

<App.Resources>
    <MainViewModel x:Key="MainViewModel" />
</App.Resources>

然后,您将能够从任何视图绑定MainViewModel命令。

<Button Command="{Binding Source={StaticResource MainViewModel}, Path=DisplayTest}" />

编辑

或者尝试这段代码:

<Button Command="{Binding DisplayTest, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window), Path=DataContext}}"/>

你的第一个解决方案是每次启动应用程序弹出一个新窗口(另一个MainWindow),但也没有解决问题。第二个解决方案无法编译。我已经尝试修复它,但它说路径设置了两次。 - Choub890
第一种解决方案是每次应用程序启动时创建MainViewModel实例,而不是MainWindow。第二种解决方案必须适应您的代码。复制粘贴不起作用。此外,仅当您在MainPage中显示子窗口时,第二种解决方案才能起作用。 - opewix
给出应该适应我的项目的代码和给出有错误的代码是不同的,这些错误使得编译变得不可能。例如,Path="DataContext" 应该是 Path=DataContextMode=FindAncestor AncestorType=... 甚至在语法上都是无效的。 - Choub890
1
我是凭记忆编写了上面的代码。在发布评论或答案之前编译每个代码将会花费太多时间。无论如何,如果您无法修复代码,只需询问即可。- 我已编辑我的答案。 - opewix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接