6

I am trying to reproduce what is suggested in Sheridan's answer to this question to navigate trough my views when using WPF with the MVVM pattern. Unfortunately, I am getting a binding error when I do so. Here is the exact error:

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')

When I look into my xaml code in LoginView.xaml, I noticed that Visual Studio is telling me that it cannot find DataContext.DisplayText in context of type MainViewModel. I have tried removing DataContext. and just keeping DisplayText instead, but to no avail.

Unless Sheridan's answer has an error, I am most definitely missing something here. What should I do for it to work?

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 is just a class that implements the INotifyPropertyChanged interface, and Test.xaml and TestViewModel.cs are just a dummy viewmodel/view for test purposes.

Community
  • 1
  • 1
Choub890
  • 1,163
  • 1
  • 13
  • 27

3 Answers3

1

MainViewModel is not a direct ancestor in the visual or logical tree, which is why RelativeSource={RelativeSource AncestorType={x:Type vm:MainViewModel}} cannot find it.

How do you fix it? First, please don't try and reach through various UI components like this to trigger commands. Just because you saw it somewhere on the internet doesn't mean it is a desirable design choice. Doing this means the LoginView has a deep understanding of other views and view models - which is bad. If you are going to do that then you might as well code everything as one single UI class with a single viewmodel that is really just a massive code behind class.

A better (but still not optimal) approach is to have the MainView (or viewmodel) spawn the LoginView. As it holds the reference to the view, it is also responsible for disposing of it. So the LoginView can be shown to collect credentials, then the main view can dispose if it signals that the credentials are validated successfully. Or it can just collect credentials and leave it up to the MainView/viewmodel to validate them (which can be done by the MainView/viewmodel triggering a background call to check the credentials against a store).

A simple (crude) rule of thumb is: a parent view can know about a child view, but in general the reverse should not happen. MVVM is about decoupling and segregating functionality, but instead you are tightly coupling them. Of course all this gets a whole lot more complex than what I've illustrated, but you can still do some of this while keeping it practical and not over-engineering.

So, TLDR;:

  • the LoginView (or its viewmodel) should implement its own command to deal with the button click
  • don't reach deep through the entrails of another view to trigger functionality
  • strive for SRP and de-coupled code/views
  • when using ancestor binding, look for something that's in the visual/logical tree
Community
  • 1
  • 1
slugster
  • 49,403
  • 14
  • 95
  • 145
  • This is what is happening already, my `LoginViewModel` (and view) is getting spawned by my `MainViewModel` when it is created. How would my `LoginViewModel` or View signal to `MainViewModel` that the credentials are validated though? That's why I was using the method in the linked question's answer. Also, you mention that what I am doing is bad as it couples everything together. What other method is there to navigate through Views without using `Page`s? That is what I was looking for initially before finding the linked question's answer. – Choub890 Dec 23 '14 at 05:06
  • 1
    In MVVM style, ViewModels interact with each other using direct event handling (if one VM creates another VM) or using `Messenger` class (if VM's are independent). Messenger has public event which is fired in method `Messenger.Send(eventArgs)`. Each view model registers event handler for Messenger's public event. Look at MVVM light toolkit. – opewix Dec 23 '14 at 05:46
  • 1
    To navigate between pages use `Frame` control. Place a frame inside MainWindow, save reference to the instance of frame and navigate using `Frame.NavigateTo` method. – opewix Dec 23 '14 at 05:50
  • Thanks for your answers so far. Could you point me to a good tutorial on how to navigate using event handling? I have google'd a lot and all that seems to pop up with "mvvm view navigation" is the method described in the question's answer linked to this question.. This is another reason why I thought it was the way to do it. Thanks again – Choub890 Dec 23 '14 at 07:21
  • http://stackoverflow.com/questions/16993918/mvvm-light-messenger-sending-and-registering-objects – opewix Dec 23 '14 at 08:57
  • @Choub890 You can do that with a boolean property (to keep it simple) or a dialog result (to get a bit more complex). Direct event handling is an option as mentioned by Jesse, but messaging is more for totally disconnected objects, not ones that have a parent-child relationship. – slugster Dec 23 '14 at 10:28
1

In my answer, I stated that you should declare your view model DataTemplates in App.xaml so that every view will have access to them. Putting them in the MainWindow class is your first problem.

Another mistake is your Binding Path for your ICommand. If you want to access something from the view model that is set as the Window.DataContext, then you should not use a RelativeSource Binding . Try this instead:

<Button Content="Create new user" Grid.Column="3" Grid.Row="3" Margin="5" Padding="5" 
    Command="{Binding DataContext.DisplayTest}, Mode=OneWay}" />

Also remember that for whatever reason, you chose not to make your MainViewModel class extend the ViewModelNavigationBase class... that could also cause you problems.

Anyway, if that doesn't sort out your problems, just let me know. Also, if you want to notify a user at anytime on Stack Overflow, just put an @ symbol in front of their name and they will receive a notification. You could have asked me this question directly if you had done that.

Sheridan
  • 68,826
  • 24
  • 143
  • 183
0

Define MainViewModel in App scope as a static resource.

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

Then you will be able to bind MainViewModel commands from any view.

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

EDIT

Or try this code:

<Button Command="{Binding DisplayTest, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window), Path=DataContext}}"/>
opewix
  • 4,993
  • 1
  • 20
  • 42
  • Your first solution is making a new window (another MainWindow) pop up each time I start the app and does not solve the problem either. The second solution does not compile. I have tried fixing it, but it says the Path set 2 times. – Choub890 Dec 23 '14 at 04:49
  • The first solution creates `MainViewModel` instance each time application starts. Not `MainWindow`. The second solution must be adapted to your code. Copy-paste won't work. Also, second solution will work only if you're displaying child window within MainPage. – opewix Dec 23 '14 at 04:53
  • There is a difference between giving a code that should be adapted to my project, and giving code with mistakes in it making it impossible to compile. For one, `Path="DataContext"` should be `Path=DataContext`, `Mode=FindAncestor AncestorType=...` is not even valid syntactically, etc. – Choub890 Dec 23 '14 at 05:08
  • 1
    I wrote the code above from my mind. It will take too much time to compile each code before posting comment or answer. Anyway, If you can't fix the code, just ask. - I've edited my answer. – opewix Dec 23 '14 at 05:31