绑定时DataItem = null,找不到原因?

时间:2014-12-23 04:10:10

标签: c# wpf mvvm data-binding navigation

我正在尝试重现Sheridan's answer to this question中建议的内容,以便在将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告诉我它在类型DataContext.DisplayText的上下文中找不到MainViewModel。我尝试删除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 个答案:

答案 0 :(得分:1)

MainViewModel不是visual or logical tree中的直接祖先,这就是RelativeSource={RelativeSource AncestorType={x:Type vm:MainViewModel}}无法找到它的原因。

你是如何解决的?首先,请不要尝试通过这样的各种UI组件来触发命令。仅仅因为你在互联网上的某个地方看到它并不意味着它是一个理想的设计选择。这样做意味着LoginView对其他视图和视图模型有深刻的理解 - 这是错误。如果您打算这样做,那么您可以将所有内容编码为一个单独的UI类,其中只有一个视图模型,它实际上只是一个庞大的代码背后的类。

更好(但仍然不是最佳)的方法是让MainView(或viewmodel)产生LoginView。由于它持有对视图的引用,因此它还负责处理它。因此,可以显示LoginView以收集凭据,然后主视图可以处理,如果它表示凭证已成功验证。或者它可以只收集凭据并将其留给MainView / viewmodel来验证它们(可以通过MainView / viewmodel触发后台调用来检查商店的凭据)。

一个简单(粗略)的经验法则是:父视图可以知道子视图,但通常不会发生相反的情况。 MVVM大概是decoupling and segregating functionality,但你是tightly coupling。当然,所有这些都比我所说的要复杂得多,但你仍然可以做到这一点,同时保持其实用性而不是过度工程化。

所以,TLDR;:

答案 1 :(得分:1)

在我的回答中,我声明您应该在DataTemplate中声明您的视图模型App.xaml,以便每个视图都可以访问它们。将它们放入MainWindow课程是你的第一个问题。

另一个错误是你Binding Path的{​​{1}}。如果您想要从设置为ICommand的视图模型中访问某些内容,那么您应该使用Window.DataContext。试试这个:

RelativeSource Binding

还要记住,无论出于何种原因,您选择 not 使<Button Content="Create new user" Grid.Column="3" Grid.Row="3" Margin="5" Padding="5" Command="{Binding DataContext.DisplayTest}, Mode=OneWay}" /> 课程延长MainViewModel课程......这也可能会导致您出现问题。

无论如何,如果这不能解决您的问题,请告诉我。此外,如果您想在Stack Overflow上随时通知用户,只需在其名称前加上ViewModelNavigationBase符号,他们就会收到通知。如果你这样做,你可以直接问我这个问题。

答案 2 :(得分:0)

将App范围中的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}}"/>