How to send an object to MainWindowViewModel using an child element view model inside a frame(WPF)?

430 views Asked by At

I have a MainWindowViewModel and my MainWindow contains a frame to display project pages. The first page being displayed is a list of recently opened projects(Similar to Microsoft word) which has it's own ViewModel. There is no problem in loading the list but when I want to send the user-selected item from this list to the MainWindowViewModel I can not use Find-Ancestor to reach the Window DataContext(It looks like the frame has some restrictions).

How can I send the user-selected item to the MainWindowViewModel?

 public class RecentlyOpenedFilesViewModel 
{


    readonly IFileHistoryService _fileHistoryService;

    private ObservableCollection<RecentlyOpenedFileInfo> _RecentlyOpenedFilesList;


    public ObservableCollection<RecentlyOpenedFileInfo> RecentlyOpenedFilesList
    {
        get { return _RecentlyOpenedFilesList; }
        set { _RecentlyOpenedFilesList = value; RaisePropertyChanged(); }
    }

    public RecentlyOpenedFilesViewModel( IFileHistoryService fileService):base()
    {
        _fileHistoryService = fileService;
        RecentlyOpenedFilesList=new ObservableCollection<RecentlyOpenedFileInfo>(_fileHistoryService.GetFileHistory());
    }

    public void RefreshList()
    {
        RecentlyOpenedFilesList = new ObservableCollection<RecentlyOpenedFileInfo>(_fileHistoryService.GetFileHistory());
    }

 
}


<Page
x:Class="MyProject.Views.V3.Other.RecentlyOpenedFilesPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyProject.Views.V3.Other"
xmlns:vmv3="clr-namespace:MyProject"
Title="RecentlyOpenedFilesPage">
<Page.Resources>
    <DataTemplate x:Key="RecentlyOpenedFileInfoTemplate"
       >
        <Button
            Height="70"
            Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}}, Path=DataContext.OpenProjectFromPathCommand}"
            CommandParameter="{Binding}">
            <Button.Content>
                <Grid>
                    <Grid.RowDefinitions>

                        <RowDefinition Height="70" />

                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="200" />
                    </Grid.ColumnDefinitions>


                    <StackPanel
                        Grid.Row="0"
                        Grid.Column="0"
                        VerticalAlignment="Top">
                        <TextBlock Text="{Binding Name}" />
                        <TextBlock Text="{Binding Path}" />
                    </StackPanel>
                    <TextBlock
                        Grid.Row="0"
                        Grid.Column="1"
                        Margin="50,0,0,0"
                        VerticalAlignment="Center"
                        Text="{Binding DateModified}" />


                </Grid>
            </Button.Content>
        </Button>
    </DataTemplate>
</Page.Resources>
<Grid>
    <ListView
        ItemTemplate="{StaticResource RecentlyOpenedFileInfoTemplate}"
        ItemsSource="{Binding RecentlyOpenedFilesList}" />

</Grid>
       public RecentlyOpenedFilesPage(MainWindowViewModel vm)
    {
        this.DataContext = vm;
        InitializeComponent();
       
    }

Now I have a direct link between MainWindowViewModel and RecentlyOpenedFilesViewModel but I would like to remove this dependency and use another way of connection like(routed commands which I have a problem with) The MainWindow contains a frame in which the RecentlyOpenedFilesPage is set to its content.

<Window    
x:Class="MyProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"    
xmlns:fw="clr-namespace:SourceChord.FluentWPF;assembly=FluentWPF" >
<Frame Name="frameMain"/></Window>

    public class MainWindowViewModel : RecentlyOpenedFilesViewModel, IMainWindowViewModel
    {
  

        private void LoadRecentlyOpenedProjects()
        {
         
            CurrentView = new RecentlyOpenedFilesPage(this);
        }

    }
1

There are 1 answers

2
grek40 On

So, here is my suggested solution. It uses the basic idea to propagate the DataContext from the outside into a frame content, as presented in page.DataContext not inherited from parent Frame?

For demonstration purpose, I provide an UI with a button to load the page, a textblock to display the selected result from the list within the page and (ofcourse) the frame that holds the page.

<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"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid Name="parentGrid">
        <TextBlock VerticalAlignment="Top" HorizontalAlignment="Right" Margin="5" Text="{Binding SelectedFile}" Width="150" Background="Yellow"/>
        <Button VerticalAlignment="Top" HorizontalAlignment="Left" Margin="5" Click="Button_Click" Width="150">Recent Files List</Button>
        <Frame Name="frameMain" Margin="5 50 5 5"
               LoadCompleted="frame_LoadCompleted"
               DataContextChanged="frame_DataContextChanged"/>
    </Grid>
</Window>

Viewmodel classes:

public class BaseVm : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName]string propName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }
}

public class MyWindowVm : BaseVm
{
    private string _selectedFile;
    public string SelectedFile
    {
        get => _selectedFile;
        set
        {
            _selectedFile = value;
            OnPropertyChanged();
        }
    }
}

public class MyPageVm : BaseVm
{
    public ObservableCollection<MyRecentFile> Files { get; } = new ObservableCollection<MyRecentFile>();
}

public class MyRecentFile
{
    public string Filename { get; set; }

    public string FilePath { get; set; }
}

Main code behind:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        parentGrid.DataContext = new MyWindowVm();
    }

    // Load Page on some event
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        frameMain.Content = new RecentlyOpenedFilesPage(new MyPageVm
        {
            Files =
            {
                new MyRecentFile { Filename = "Test1.txt", FilePath = "FullPath/Test1.txt"},
                new MyRecentFile { Filename = "Test2.txt", FilePath = "FullPath/Test2.txt"}
            }
        });
    }

    // DataContext to Frame Content propagation
    private void frame_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        UpdateFrameDataContext(sender as Frame);
    }
    private void frame_LoadCompleted(object sender, NavigationEventArgs e)
    {
        UpdateFrameDataContext(sender as Frame);
    }
    private void UpdateFrameDataContext(Frame frame)
    {
        var content = frame.Content as FrameworkElement;
        if (content == null)
            return;
        content.DataContext = frame.DataContext;
    }
}

Now, the page.xaml ... notice: we will set the page viewmodel to the pageRoot.DataContext, not to the page itself. Instead we expect the page datacontext to be handled from the outside (as we do in the MainWindow) and we can reference it with the page internal name _self:

<Page x:Class="WpfApplication1.RecentlyOpenedFilesPage"
      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" 
      mc:Ignorable="d" 
      d:DesignHeight="450" d:DesignWidth="800"
      Title="RecentlyOpenedFilesPage"
      Name="_self">

    <Grid Name="pageRoot">
        <ListView ItemsSource="{Binding Files}"
                  SelectedValue="{Binding DataContext.SelectedFile,ElementName=_self}"
                  SelectedValuePath="FilePath"
                  DisplayMemberPath="Filename"/>
    </Grid>
</Page>

Page code behind to wire up the viewmodel:

public partial class RecentlyOpenedFilesPage : Page
{
    public RecentlyOpenedFilesPage(MyPageVm myPageVm)
    {
        InitializeComponent();

        pageRoot.DataContext = myPageVm;
    }
}

As you can see, with this setup, no viewmodel knows about any involved view. The page doesn't handle the MainViewmodel, but the page requires a DataContext with a SelectedFile property to be provided from the outside. The MainViewmodel doesn't know about the recent file list, but allows to set a selected file, no matter where it originates from.

The decision, to initialize the RecentlyOpenedFilesPage with a pre-created viewmodel is not important. You could just as well use internal logic to initialize the page with recent files, then the Mainwindow would not be involved.