0

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);
        }

    }
  • Not enough code to understand how the relevant parts work together – grek40 Oct 07 '20 at 12:02
  • Thank you for your reply, @grek40 I tried to provide more code, Let me know if I can provide more details. – hamid ramezanali Oct 08 '20 at 07:06
  • Since you set the Page `DataContext` to `MainWindowViewModel`, where do you set the `RecentlyOpenedFilesViewModel`? Also, have a look at [page.DataContext not inherited from parent Frame?](https://stackoverflow.com/a/3643716/5265292), it might be related – grek40 Oct 08 '20 at 09:17
  • I Inherited MainWindowViewMoel from RecentlyOpenedFilesViewModel and I know it is not the solution but it works. I added the inheritance to the rest of the code – hamid ramezanali Oct 08 '20 at 10:57
  • Sorry, but you are **not using the MVVM pattern**, when your viewmodel knows about your view as in `CurrentView = new RecentlyOpenedFilesPage(this);`. Such a deviation from the MVVM pattern makes it really hard to understand how your application is intended to work. – grek40 Oct 08 '20 at 11:18
  • @grek Thanks for replying to this. That is exactly the question. How can I remove that connection? I couldn't find any other way to communicate and it seems the problem is the "FRAME" element. it doesn't allow routed commands to get to the MainWindow.DataContext – hamid ramezanali Oct 08 '20 at 11:51

1 Answers1

0

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.

grek40
  • 13,113
  • 1
  • 24
  • 50
  • Thank you for your detailed reply @grek40. I tried to change my code based on your code and comments above(replaced CurrentView = new RecentlyOpenedFilesPage(this); with CurrentViewModel = ViewModelFactory.GetViewModel() ) and now I think it is way more compatible with MVVM. – hamid ramezanali Oct 09 '20 at 10:31
  • @hamidramezanali If this answer solves your question, you can [accept it as an answer](https://meta.stackexchange.com/q/5234/397416) – grek40 Oct 09 '20 at 10:55