在WPF中使用MVVM,我需要导航到包含父对象子对象的另一个视图。

3
我正在开发一个WPF项目,尝试在视图中不使用任何代码来遵循MVVM概念。
总之,我有一个网格列表,其中列出了作业对象的属性列表,当我点击每个网格行内的“显示日志”按钮时,我希望它向我展示另一个包含此作业日志的网格,而不会破坏MVVM概念。
我只想显示另一个包含子属性的网格,该子属性是一个对象列表,在所有其他技术(MVC、MVP)中都很简单,但在MVVM中却有些奇怪。我搜索了大约20个问题,没有直接的解决方案。

enter image description here

细节: 我有一个MainView.xaml(窗口),JobsView.xaml(用户控件),LogsView.xaml(用户控件),并且每个视图都有相应的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(用户控件),用于列出所有作业对象的属性,并为每个作业创建了一个自定义按钮,以显示日志。
<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>

当任何人点击“显示日志”按钮时,它应该在MainView.xaml中显示LogsView.xaml用户控件,而不是JobsView。

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


}

但是现在我正在尝试制作一个导航服务并尝试一些技术,但它没有起作用。

有没有答案?:( - Marzouk
请尝试在此处提问:https://social.msdn.microsoft.com/Forums/vstudio/en-US/home?forum=wpf。 - StepUp
你是否在寻找“RowDetailTemplate”?http://www.wpf-tutorial.com/datagrid-control/details-row/ - Eldho
2个回答

3

类似这样的东西可能有用:WPF MVVM导航视图

<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,然后将其设置为MainViewModel中的ViewModel属性。

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
1
@Marzouk 我更新了我的答案。 - Tim Pohlmann
我想声明,在JobsUserControl(JobView)中,“show logs”按钮的Command =“{Binding ShowLogsCommand}”将在JobViewModel中定义,这是我的主要问题。 - Marzouk
1
你可以在MainViewModel中定义命令并相应地设置绑定,或者在JobViewModel中引用MainViewModel。 - Tim Pohlmann
我认为这就是我一开始尝试做的事情,但不幸的是它没有成功。我只想显示另一个网格,其中包含所选对象的子对象,这在所有其他技术中都是直截了当的简单事情,但在MVVM中却有些奇怪,我搜索了大约20个问题,但没有找到直接的解决方案。 - Marzouk
@Marzouk 我提出的方法是可行的,但我同意在处理程序流程时,MVVM 可能有点繁琐。 - Tim Pohlmann

1

请尝试下一个解决方案:

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 JobModel{Id = 1, Salary = "12k", Title = "Hw Engineer"},
            new JobModel{Id=2, Salary = "18k", Title = "Sw Engineer"},
            new JobModel{Id = 3, Salary = "12k", Title = "IT Engineer"},
            new JobModel{Id=4, Salary = "18k", Title = "QA Engineer"},
        };

        _logModels = new List<LogModel>
        {
            new LogModel{Id = 1, Salary = "12k", Title = "Hw Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Pending"},
            new LogModel{Id = 1, Salary = "12k", Title = "Hw Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Active"},
            new LogModel{Id = 1, Salary = "12k", Title = "Hw Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Closed"},
            new LogModel{Id=2, Salary = "12k", Title = "Sw Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Pending"},
            new LogModel{Id=2, Salary = "12k", Title = "Sw Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Active"},
            new LogModel{Id=2, Salary = "12k", Title = "Sw Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Closed"},
            new LogModel{Id = 3, Salary = "12k", Title = "IT Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Pending"},
            new LogModel{Id = 3, Salary = "12k", Title = "IT Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Active"},
            new LogModel{Id = 3, Salary = "12k", Title = "IT Engineer", LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Closed"},
            new LogModel{Id=4, Salary = "12k", Title = "QA Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Pending"},
            new LogModel{Id=4, Salary = "12k", Title = "QA Engineer",   LogTime = DateTime.Now.ToLocalTime(), LogEvent = "Active"},
            new LogModel{Id=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实现和中继命令代码。
/// <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;
}

简要说明

  1. 我们有三个DataTemplate:Jobs、Logs和Default。
  2. DataTemplateSelector将根据附加到其中的ContentControl的Content属性管理数据模板选择。
  3. 按钮位于网格内,并绑定到父VM的命令。通过Freezable代理对象,将父VM提供给DataTemplate。

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

敬礼。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接