3

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

enter image description here

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.

Marzouk
  • 2,650
  • 3
  • 25
  • 56

2 Answers2

3

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}"
/>
Community
  • 1
  • 1
Tim Pohlmann
  • 4,140
  • 3
  • 32
  • 61
  • i tried that but it fails, can you put a sample example of ShowLogsCommand () implementation please – Marzouk Feb 18 '16 at 14:18
  • 1
    @Marzouk Updated my answer. – Tim Pohlmann Feb 18 '16 at 14:28
  • I want to declare that the 'show logs' button in the JobsUserControl (JobView) so Command="{Binding ShowLogsCommand}" will be defined in JobViewModel and this is my main problem – Marzouk Feb 18 '16 at 14:56
  • 1
    You can either define the command in MainViewModel and set your binding accordingly, or make the MainViewModel known in your JobViewModel. – Tim Pohlmann Feb 18 '16 at 14:58
  • i think this is what i tried to do from the beginning, but unfortunately it didn't work. I only want to show another grid contains a child objects of the selected object, it's straightforward easy thing in all other techniques but here in MVVM it's some sort of strange, i searched for that for about 20 questions and no straightforward solution – Marzouk Feb 18 '16 at 15:37
  • @Marzouk It's possible with the way I proposed, but I agree that MVVM can be a bit tedious when it comes to handling the program flow. – Tim Pohlmann Feb 18 '16 at 16:04
1

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.

Ilan
  • 2,762
  • 1
  • 13
  • 24