简体   繁体   中英

I need to navigate to another view contains child objects of parent object in WPF with MVVM

I working on WPF project and trying to not break MVVM concepts with zero code in Views.

In conclusion i have a grid lists a list of job object properties and i want when i click on show logs button inside every grid row it shows to me another grid which contains logs for this job without breaking MVVM concept.

I only want to show another grid contains a child property which is a list of objects, it's straightforward easy thing in all other techniques MVC, MVP but here in MVVM it's some sort of strange, i searched for that for about 20 questions and no straightforward solution

在此输入图像描述

Details: I have a MainView.xaml (Window), JobsView.xaml (UserControl), LogsView.xaml(UserControl) and i have corresponding ViewModel for each one.

Job class contains id, status, ... and a list of Log object:

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

I shown a JobsView.xaml (UserControl) in the MainView.xaml to list all job objects properties and i created a custom button for each job to shown logs.

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

JobView.xaml markup:

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

I want when any body click on Show Logs button it shown LogsView.xaml user control inside MainView.xaml instead of JobsView.

In LogViewModel i have a contructor to take jobId and return the logs:

    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;


}

But now i trying to make a navigation service and tries some techniques but it didn't works.

Something like this might work: 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>

Then write the ShowLogsCommand so that it creates a new LogViewModel based on the currently selected job and then sets it to the ViewModel property (in MainViewModel ).
Make sure to properly implement INotifyPropertyChanged.

Example for ShowLogsCommand (I did not test this, use with care):

ICommand ShowLogsCommand => new RelayCommand(showLogsCommand);

private void showLogsCommand(Job job)
{
    ViewModel = new LogViewModel(job.JobId);
}

Change the xaml to:

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

please try the next solution:

Xaml (is based on data template selector)

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

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 implementation and Relay command code

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

Small explanation

  1. We have three DataTemplate Jobs, Logs, Default.
  2. The DataTemplateSelector will manage the data template selection based on the Content property of the ContentControl which the DataTemplateSelector is attached to it.
  3. The button is inside the grid and it is bound to the command from the parent's VM. The parent VM is provided to the DataTemplate via Freezable proxie object.

Let me know if you have problems with the code.

Regards.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM