【问题标题】:How to send an object to MainWindowViewModel using an child element view model inside a frame(WPF)?如何使用框架内的子元素视图模型(WPF)将对象发送到 MainWindowViewModel?
【发布时间】:2020-10-07 08:32:57
【问题描述】:

我有一个 MainWindowViewModel,我的 MainWindow 包含一个显示项目页面的框架。 显示的第一页是最近打开的项目列表(类似于 Microsoft Word),它有自己的 ViewModel。 加载列表没有问题,但是当我想将此列表中的用户选择的项目发送到 MainWindowViewModel 时,我无法使用 Find-Ancestor 到达 Window DataContext(看起来框架有一些限制)。

如何将用户选择的项目发送到 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();
       
    }

现在我在 MainWindowViewModel 和 RecentOpenedFilesViewModel 之间建立了直接链接,但我想删除这种依赖关系并使用另一种连接方式,例如(我遇到问题的路由命令) MainWindow 包含一个框架,在该框架中将 RecentOpenedFilesPage 设置为其内容。

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

    }

【问题讨论】:

  • 没有足够的代码来理解相关部分是如何协同工作的
  • 感谢您的回复,@grek40 我试图提供更多代码,如果我可以提供更多详细信息,请告诉我。
  • 既然您将页面DataContext 设置为MainWindowViewModel,那么您在哪里设置RecentlyOpenedFilesViewModel?另外,看看page.DataContext not inherited from parent Frame?,可能是相关的
  • 我从 RecentOpenedFilesViewModel 继承了 MainWindowViewMoel,我知道这不是解决方案,但它确实有效。我将继承添加到其余代码中
  • 抱歉,当您的视图模型知道您在 CurrentView = new RecentlyOpenedFilesPage(this); 中的视图时,您没有使用 MVVM 模式。与 MVVM 模式的这种偏差使您很难理解您的应用程序是如何工作的。

标签: c# wpf mvvm viewmodel mainwindow


【解决方案1】:

所以,这是我建议的解决方案。它使用基本思想将DataContext从外部传播到框架内容中,如page.DataContext not inherited from parent Frame?中所示

出于演示目的,我提供了一个 UI,其中包含一个用于加载页面的按钮、一个用于显示从页面内的列表中选择的结果的文本块以及(当然)包含该页面的框架。

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

视图模型类:

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

后面的主要代码:

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

现在,page.xaml ... 注意:我们将页面视图模型设置为pageRoot.DataContext,而不是页面本身。相反,我们希望从外部处理页面数据上下文(就像我们在 MainWindow 中所做的那样),我们可以使用页面内部名称 _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>

用于连接视图模型的页面代码:

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

        pageRoot.DataContext = myPageVm;
    }
}

如您所见,通过此设置,没有任何视图模型知道任何涉及的视图。该页面不处理 MainViewmodel,但该页面需要从外部提供具有 SelectedFile 属性的 DataContext。 MainViewmodel 不知道最近的文件列表,但允许设置选定的文件,无论它来自哪里。

用预先创建的视图模型初始化RecentlyOpenedFilesPage 的决定并不重要。您也可以使用内部逻辑来使用最近的文件初始化页面,然后不会涉及主窗口。

【讨论】:

  • 感谢您的详细回复@grek40。我尝试根据您的代码和上面的 cmets 更改我的代码(将 CurrentView = new RecentOpenedFilesPage(this); 替换为 CurrentViewModel = ViewModelFactory.GetViewModel() ),现在我认为它与 MVVM 更兼容。
  • @hamidramezanali 如果这个答案解决了你的问题,你可以accept it as an answer
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-12
  • 1970-01-01
  • 1970-01-01
  • 2012-12-23
相关资源
最近更新 更多