我需要导航到另一个视图,其中包含带有 MVVM 的 WPF 中父对象的子对象

Posted

技术标签:

【中文标题】我需要导航到另一个视图,其中包含带有 MVVM 的 WPF 中父对象的子对象【英文标题】:I need to navigate to another view contains child objects of parent object in WPF with MVVM 【发布时间】:2016-05-30 16:20:50 【问题描述】:

我从事 WPF 项目,并试图在 Views 中使用零代码不破坏 MVVM 概念。

总之,我有一个网格列出了作业对象属性的列表,当我单击每个网格行内的显示日志按钮时,它会向我显示另一个网格,其中包含该作业的日志,而不会破坏 MVVM 概念。

我只想显示另一个网格包含一个子属性,它是一个对象列表,这在所有其他技术 MVC、MVP 中是很简单的事情,但在 MVVM 中它有点奇怪,我搜索了大约 20 个问题并且没有直接的解决方案

详情: 我有一个 MainView.xaml (Window)、JobsView.xaml (UserControl)、LogsView.xaml(UserControl),每个都有对应的 ViewModel。

Job 类包含 id、status、...和一个 Log 对象列表:

 public class Job

    public Job()
    
        Logs = new List<Log>();
    
    [Key]
    public Guid JobID  get; set; 
    public JobStatus Status  get; set; 
    public virtual ICollection<Log> Logs  get; set; 

我在 MainView.xaml 中显示了 JobsView.xaml (UserControl) 以列出所有作业对象属性,并为每个作业创建了一个自定义按钮以显示日志。

<Controls:MetroWindow ...>
<Grid>
    <DockPanel>
             <my:JobView />
    </DockPanel>
</Grid>

JobView.xaml 标记:

<UserControl x:Class=...>
<Grid>
    <DataGrid x:Name="jobsDataGrid"
              ItemsSource="Binding Jobs"
              SelectedItem="Binding selectedJob"
              AutoGenerateColumns="False"
              EnableRowVirtualization="True"
              RowDetailsVisibilityMode="VisibleWhenSelected"
              IsReadOnly="True">
                <DataGrid.Columns>
            <DataGridTextColumn x:Name="jobIdColumn"
                                Binding="Binding JobID"
                                Header="Job Id"
                               Width="SizeToHeader"
                                />

            <DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Button Content="Show Logs"
                                        Command="Binding ShowLogsCommand"
                                        />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>
</Grid>

我希望当任何主体单击 Show Logs 按钮时,它会在 MainView.xaml 而不是 JobsView 中显示 LogsView.xaml 用户控件。

在 LogViewModel 中,我有一个构造函数来获取 jobId 并返回日志:

    public class LogViewModel : BindableBase // INotifyPropertyChanged

    private Log log = new Log();
    private UnitOfWork unitOfWork = new UnitOfWork();

    public LogViewModel()
    
        if (DesignerProperties.GetIsInDesignMode(new System.Windows.DependencyObject())) return;
        Logs = new ObservableCollection<Log>(unitOfWork.Logs.Get(null, ls => ls.OrderBy(l => l.LogID)).ToList());
    

    public LogViewModel(Guid jobId)
    
        if (DesignerProperties.GetIsInDesignMode(new System.Windows.DependencyObject())) return;
        Logs = new ObservableCollection<Log>(unitOfWork.Logs.Get(l => l.JobID == jobId, ls => ls.OrderBy(l => l.LogID)).ToList());
    


    public ObservableCollection<Log> Logs  get; set; 


  //  public event PropertyChangedEventHandler PropertyChanged;



但现在我尝试制作导航服务并尝试了一些技术但没有成功。

【问题讨论】:

有答案吗:( 尝试在这里问social.msdn.microsoft.com/Forums/vstudio/en-US/home?forum=wpf 您在寻找RowDetailTemplate 吗? wpf-tutorial.com/datagrid-control/details-row 【参考方案1】:

这样的事情可能会起作用:WPF MVVM navigate views

<Controls:MetroWindow ...>
<Controls:MetroWindow.Resources>
    <DataTemplate DataType="x:Type my:LogViewModel">
        <my:LogView/>
    </DataTemplate>
    <DataTemplate DataType="x:Type my:JobViewModel">
        <my:JobView/>
    </DataTemplate>
</Controls:MetroWindow.Resources>
<Grid>
    <DockPanel>
        <ContentControl Content="Binding ViewModel" />
    </DockPanel>
</Grid>

然后编写ShowLogsCommand,以便它根据当前选定的作业创建一个新的LogViewModel,然后将其设置为ViewModel 属性(在MainViewModel 中)。 确保正确实现 INotifyPropertyChanged。

ShowLogsCommand 的示例(我没有对此进行测试,请谨慎使用):

ICommand ShowLogsCommand => new RelayCommand(showLogsCommand);

private void showLogsCommand(Job job)

    ViewModel = new LogViewModel(job.JobId);

将 xaml 更改为:

<Button Content="Show Logs"
        Command="Binding ShowLogsCommand"
        CommandParameter="Binding"
/>

【讨论】:

我试过了,但失败了,你能举一个 ShowLogsCommand () 实现的示例 @Marzouk 更新了我的答案。 我想声明 JobsUserControl (JobView) 中的“显示日志”按钮,所以 Command="Binding ShowLogsCommand" 将在 JobViewModel 中定义,这是我的主要问题 您可以在 MainViewModel 中定义命令并相应地设置绑定,或者在 JobViewModel 中使 MainViewModel 为已知。 我认为这是我从一开始就尝试做的,但不幸的是它没有奏效。我只想显示另一个网格包含所选对象的子对象,这在所有其他技术中都很简单,但在 MVVM 中它有点奇怪,我搜索了大约 20 个问题并且没有直接的解决方案【参考方案2】:

请尝试下一个解决方案:

Xaml(基于数据模板选择器)

<Window x:Class="MvvmNavigationIssue.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mvvmNavigationIssue="clr-namespace:MvvmNavigationIssue"
    Title="MainWindow" Height="350" Width="525" x:Name="This">
<Window.DataContext>
    <mvvmNavigationIssue:MainNavigationViewModel/>
</Window.DataContext>
<Window.Resources>
    <mvvmNavigationIssue:FreezableProxyClass x:Key="ProxyElement" 
                                             ProxiedDataContext="Binding Source=x:Reference This, Path=DataContext"/>
    <DataTemplate x:Key="DefaultDataTemplate">
        <Grid>
            <Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="Tomato" />
            <TextBlock Text="Default Template" HorizontalAlignment="Center" VerticalAlignment="Center"></TextBlock>
        </Grid>
    </DataTemplate>
    <DataTemplate x:Key="JobsDataTemplate">
        <ListView ItemsSource="Binding JobModels, UpdateSourceTrigger=PropertyChanged">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Id" DisplayMemberBinding="Binding Id, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged" Width="100">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate DataType="mvvmNavigationIssue:JobModel">
                                <TextBlock Text="Binding Id"></TextBlock>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn Header="Title" DisplayMemberBinding="Binding Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged" Width="100"/>
                    <GridViewColumn Header="Salary" DisplayMemberBinding="Binding Salary, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged" Width="100"/>
                    <GridViewColumn Header="" Width="100">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate DataType="mvvmNavigationIssue:JobModel">
                                <Button Command="Binding Source=StaticResource ProxyElement, 
                                    Path=ProxiedDataContext.ShowLogsCommand, Mode=OneWay, 
                                    UpdateSourceTrigger=PropertyChanged" CommandParameter="Binding ">Logs</Button>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
    </DataTemplate>
    <DataTemplate x:Key="LogsDataTemplate">
        <ListView ItemsSource="Binding LogModels, UpdateSourceTrigger=PropertyChanged">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Id" DisplayMemberBinding="Binding Id, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged" Width="100">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate DataType="mvvmNavigationIssue:JobModel">
                                <TextBlock Text="Binding Id"></TextBlock>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn Header="Title" DisplayMemberBinding="Binding Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged" Width="100"/>
                    <GridViewColumn Header="Time" DisplayMemberBinding="Binding LogTime, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged" Width="100"/>
                    <GridViewColumn Header="Event" DisplayMemberBinding="Binding LogEvent, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged" Width="100"/>
                    <GridViewColumn Header="" Width="100">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate DataType="mvvmNavigationIssue:JobModel">
                                <Button Command="Binding Source=StaticResource ProxyElement, 
                                    Path=ProxiedDataContext.ShowAllJobsCommand, Mode=OneWay, 
                                    UpdateSourceTrigger=PropertyChanged">All Jobs</Button>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
    </DataTemplate>
    <mvvmNavigationIssue:MainContentTemplateSelector x:Key="MainContentTemplateSelectorKey" 
                                                     DefaultDataTemplate="StaticResource DefaultDataTemplate"
                                                     JobsViewDataTemplate="StaticResource JobsDataTemplate"
                                                     LogsViewDataTemplate="StaticResource LogsDataTemplate"/>
</Window.Resources>
<Grid>
    <ContentControl Content="Binding CurrentViewModel, UpdateSourceTrigger=PropertyChanged"
                    ContentTemplateSelector="StaticResource MainContentTemplateSelectorKey"></ContentControl>
</Grid>

MVVM 代码

public class FreezableProxyClass : Freezable

    protected override Freezable CreateInstanceCore()
    
        return new FreezableProxyClass();
    


    public static readonly DependencyProperty ProxiedDataContextProperty = DependencyProperty.Register(
        "ProxiedDataContext", typeof(object), typeof(FreezableProxyClass), new PropertyMetadata(default(object)));

    public object ProxiedDataContext
    
        get  return (object)GetValue(ProxiedDataContextProperty); 
        set  SetValue(ProxiedDataContextProperty, value); 
    


public class MainNavigationViewModel : BaseObservableObject

    private object _currentViewModel;
    private JobsViewModel _jobsViewModel;
    private List<LogModel> _logModels;
    private ICommand _showLogs;
    private ICommand _showJobs;

    public MainNavigationViewModel()
    
        _jobsViewModel = new JobsViewModel();
        Init();
    

    private void Init()
    
        _jobsViewModel.JobModels = new ObservableCollection<JobModel>
        
            new JobModelId = 1, Salary = "12k", Title = "Hw Engineer",
            new JobModelId=2, Salary = "18k", Title = "Sw Engineer",
            new JobModelId = 3, Salary = "12k", Title = "IT Engineer",
            new JobModelId=4, Salary = "18k", Title = "QA Engineer",
        ;

        _logModels = new List<LogModel>
        
            new LogModelId = 1, Salary = "12k", Title = "Hw Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Pending",
            new LogModelId = 1, Salary = "12k", Title = "Hw Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Active",
            new LogModelId = 1, Salary = "12k", Title = "Hw Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Closed",
            new LogModelId=2, Salary = "12k", Title = "Sw Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Pending",
            new LogModelId=2, Salary = "12k", Title = "Sw Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Active",
            new LogModelId=2, Salary = "12k", Title = "Sw Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Closed",
            new LogModelId = 3, Salary = "12k", Title = "IT Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Pending",
            new LogModelId = 3, Salary = "12k", Title = "IT Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Active",
            new LogModelId = 3, Salary = "12k", Title = "IT Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Closed",
            new LogModelId=4, Salary = "12k", Title = "QA Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Pending",
            new LogModelId=4, Salary = "12k", Title = "QA Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Active",
            new LogModelId=4, Salary = "12k", Title = "QA Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Closed",
        ;

        CurrentViewModel = _jobsViewModel;
    

    public object CurrentViewModel
    
        get  return _currentViewModel; 
        set
        
            _currentViewModel = value;
            OnPropertyChanged(()=>CurrentViewModel);
        
    

    public ICommand ShowLogsCommand
    
        get  return _showLogs ?? (_showLogs = new RelayCommand<JobModel>(ShowLogs)); 
    

    private void ShowLogs(JobModel obj)
    
        CurrentViewModel = new LogsViewModel
        
            LogModels = new ObservableCollection<LogModel>(_logModels.Where(model => model.Id == obj.Id)),
        ;
    

    public ICommand ShowAllJobsCommand
    
        get  return _showJobs ?? (_showJobs = new RelayCommand(ShowAllJobs)); 
    

    private void ShowAllJobs()
    
        CurrentViewModel = _jobsViewModel;
    


public class LogsViewModel:BaseObservableObject

    private ObservableCollection<LogModel> _logModels;

    public ObservableCollection<LogModel> LogModels
    
        get  return _logModels; 
        set
        
            _logModels = value;
            OnPropertyChanged();
        
    


public class LogModel : JobModel

    private DateTime _logTime;
    private string _logEvent;

    public DateTime LogTime
    
        get  return _logTime; 
        set
        
            _logTime = value;
            OnPropertyChanged();
        
    

    public string LogEvent
    
        get  return _logEvent; 
        set
        
            _logEvent = value;
            OnPropertyChanged();
        
    


public class JobsViewModel:BaseObservableObject

    private ObservableCollection<JobModel> _jobModels;

    public ObservableCollection<JobModel> JobModels
    
        get  return _jobModels; 
        set
        
            _jobModels = value;
            OnPropertyChanged();
        
    


public class JobModel:BaseObservableObject

    private int _id;
    private string _title;
    private string _salary;

    public int Id
    
        get  return _id; 
        set
        
            _id = value;
            OnPropertyChanged();
        
    

    public string Title
    
        get  return _title; 
        set
        
            _title = value;
            OnPropertyChanged();
        
    

    public string Salary
    
        get  return _salary; 
        set
        
            _salary = value;
            OnPropertyChanged();
        
    

INPC实现和Relay命令代码

/// <summary>
/// implements the INotifyPropertyChanged (.net 4.5)
/// </summary>
public class BaseObservableObject : INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> raiser)
    
        var propName = ((MemberExpression)raiser.Body).Member.Name;
        OnPropertyChanged(propName);
    

    protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
    
        if (!EqualityComparer<T>.Default.Equals(field, value))
        
            field = value;
            OnPropertyChanged(name);
            return true;
        
        return false;
    


public class RelayCommand : ICommand

    private readonly Func<bool> _canExecute;
    private readonly Action _execute;

    public RelayCommand(Action execute)
        : this(() => true, execute)
    
    

    public RelayCommand(Func<bool> canExecute, Action execute)
    
        _canExecute = canExecute;
        _execute = execute;
    

    public bool CanExecute(object parameter = null)
    
        return _canExecute();
    

    public void Execute(object parameter = null)
    
        _execute();
    

    public event EventHandler CanExecuteChanged;


public class RelayCommand<T> : ICommand
    where T:class 

    private readonly Predicate<T> _canExecute;
    private readonly Action<T> _execute;

    public RelayCommand(Action<T> execute):this(obj => true, execute)
    
    

    public RelayCommand(Predicate<T> canExecute, Action<T> execute)
    
        _canExecute = canExecute;
        _execute = execute;
    

    public bool CanExecute(object parameter)
    
        return _canExecute(parameter as T);
    

    public void Execute(object parameter)
    
        _execute(parameter as T);
    

    public event EventHandler CanExecuteChanged;

小解释

    我们有三个 DataTemplate 作业、日志、默认值。 DataTemplateSelector 将根据 DataTemplateSelector 附加到它的 ContentControl 的 Content 属性来管理数据模板选择。 该按钮位于网格内,并绑定到来自父虚拟机的命令。父 VM 通过 Freezable 代理对象提供给 DataTemplate。

如果您对代码有任何问题,请告诉我。

问候。

【讨论】:

以上是关于我需要导航到另一个视图,其中包含带有 MVVM 的 WPF 中父对象的子对象的主要内容,如果未能解决你的问题,请参考以下文章

带有 MVVM Light 的标签栏控制器导航 Xamarin

在 UserControl WPF MVVM caliburn 内的 UserControl 之间切换

单击搜索栏时如何导航到另一个视图控制器

标签栏包含导航和基于视图的应用程序

如何使用 MVVM Light for WPF 在窗口中导航?

使用 NavController 从片段导航到另一个片段