1

I want to add Sort functionality on top of two Filters that I have implemented.

This is what my code looks like at the moment.

XAML

<Grid Grid.Row="1">
        <StackPanel Orientation="Horizontal">
            <Border  BorderThickness="1" BorderBrush="Red">
                <ComboBox
                Name="SortComboBox"
                SelectionChanged="View_SelectionChanged"
                ItemsSource="{Binding sortOptions, Mode=OneWay}" 
                SelectedValue="{Binding selectedSortOption}"
            />
            </Border>
            <ComboBox
                Name="GenreComboBox"
                SelectionChanged="View_SelectionChanged"
                ItemsSource="{Binding genreOptions, Mode=OneWay}" 
                SelectedValue="{Binding selectedGenreOption}"
            />
            <TextBox Name="SearchTextBox" Width="154" TextChanged="Search_SelectionChanged" Text="{Binding Path=searchTerm, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
    </Grid>    

The first SortComboBox is used to sort movies while the GenreComboBox filters movies based on genre. The SearchTextBox is another filter to find movies by keywords.

Behind Code

    private void View_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        _moviePanelVM.DisplayMovies.View.Refresh();
    }

    private void Search_SelectionChanged(object sender, TextChangedEventArgs e)
    {
        _moviePanelVM.DisplayMovies.View.Refresh();
    }

ViewModel

 public MoviePanelViewModel()
    {
        ...
        ...

        this.DisplayMovies = new CollectionViewSource();
        this.DisplayMovies.Source = this.Movies;
        this.DisplayMovies.Filter += GenreFilter;
        this.DisplayMovies.Filter += SearchFilter;
    }

    private void GenreFilter(object sender, FilterEventArgs e)
    {
        MediaDetail media = e.Item as MediaDetail;
        if (selectedGenreOption != "All" && !media.genre.Contains(selectedGenreOption))
            e.Accepted = false;    
    }

    private void SearchFilter(object sender, FilterEventArgs e)
    {
        MediaDetail media = e.Item as MediaDetail;
        if (!media.title.ToLower().Contains(searchTerm.ToLower()))
            e.Accepted = false;
    }

The SortComboBox may have a selected value of A-Z in which case I would sort a particular way. Or it may have a value of Z-A in which case I would sort it another way.

My question is, where should I be adding SortDescriptions and the logic to control what kind of sort should occur in order to ensure that the MVVM pattern is maintained?

UPDATE

Code Behind

private void Sort_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        _moviePanelVM.SortMovies();
    }

Changed my XAML to:

<ComboBox
   Name="SortComboBox"
   SelectionChanged="Sort_SelectionChanged"
   ItemsSource="{Binding sortOptions, Mode=OneWay}" 
   SelectedValue="{Binding selectedSortOption}"
/>

ViewModel

public void SortMovies()
    {
        DisplayMovies.SortDescriptions.Clear();

        switch (SelectedSortOption)
        {
            case "A-Z":
                DisplayMovies.SortDescriptions.Add(new SortDescription("title", ListSortDirection.Ascending)); break;
            case "Z-A":
                DisplayMovies.SortDescriptions.Add(new SortDescription("title", ListSortDirection.Descending)); break;
            case "Release Date":
                DisplayMovies.SortDescriptions.Add(new SortDescription("year", ListSortDirection.Descending)); break;
            case "Rating":
                DisplayMovies.SortDescriptions.Add(new SortDescription("rating", ListSortDirection.Descending)); break;
        }
    }

This works but I was wondering if I should be doing it like this? As the filters were simply Refreshing the view but with sort I'm calling a function in the ViewModel.

  • Not sure I understand. You already use `CollectionViewSource` and bind to `selectedSortOption` property so when it changes clear and create new `SortDescription` for `DisplayMovies` – dkozl Jan 06 '15 at 11:18
  • @dkozl I've updated the question with the way I'm currently doing it. I just wanted to know if this is the correct way to go about this. Thanks. –  Jan 06 '15 at 12:02
  • Assuming that `SortMovies` is part of `MoviePanelViewModel` move it, for example, to the setter of `selectedSortOption` instead of calling it from UI – dkozl Jan 06 '15 at 12:05
  • first of all, if you want to keep the mvvm tag in your question I would change this `_moviePanelVM.DisplayMovies.View.Refresh();` to something that is actually MvvM. Secondly yes that would be correct as there is no way of doing this in `xaml`. HTH – XAMlMAX Jan 06 '15 at 12:24
  • @dkozl Thanks for that. I have done as you instructed –  Jan 07 '15 at 00:07
  • @XAMlMAX Could you please suggest what I should do with the `Refresh()`? Does it need to be moved or removed altogether and replaced with something else? Or maybe I can try an implement a `Command` as per the answer below.. ? Sorry for all the questions! –  Jan 07 '15 at 00:17
  • 1
    Now that I've read through your question it appears that your `ViewModel` is the actual code-behind for your View? If that's the case then, apart from wrong naming, you should be ok with what you have. But for future reference have a look at this article [MVVM](http://www.codeproject.com/Articles/100175/Model-View-ViewModel-MVVM-Explained). HTH – XAMlMAX Jan 07 '15 at 08:41
  • @XAMlMAX Actually I have named everything as it appears in my program. –  Jan 07 '15 at 14:01
  • OK, I understand you are trying to keep code-behind as clean as possible, noble task :-), but the problem is that the `ViewModel` doesn't benefit from sorted or grouped collection this is purely your `View`s concern, that would be my point in this conversation. but because I try to achieve exactly the same thing when it comes to coding I think you can get away with what you have now. To make it more MvvM though I would use [Behaviour](http://blog.functionalfun.net/2008/09/hooking-up-commands-to-events-in-wpf.html). HTH – XAMlMAX Jan 07 '15 at 14:11

1 Answers1

1

I think you are unclear on some of the principles of the MVVM design pattern. Ideally you do not want any code behind on the view at all, this includes events.

You need to replace events with commands. Click here for a tutorial.

In the following example, I am not using any events on the view, but instead I am using a command to bind the change of the sorting to filter the movies.

Here are the models I am using:

public class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class Movie : PropertyChangedBase
{
    private string _Name;

    public string Name
    {
        get { return _Name; }
        set 
        { 
            _Name = value;
            OnPropertyChanged("Name");
        }
    }
}

Here is the command:

public class SortChangedCommand : ICommand
{
    MoviesViewModel _viewModel;

    public SortChangedCommand(MoviesViewModel viewModel)
    {
        _viewModel = viewModel;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        _viewModel.Sort(parameter as string);
    }
}

Note that the command holds a reference to the view model, and it simply calls the Sort method when the command gets executed.

Here is the view model:

public class MoviesViewModel : PropertyChangedBase
{
    public ObservableCollection<Movie> Movies { get; set; }

    private ObservableCollection<Movie> _FilteredMovies;

    public ObservableCollection<Movie> FilteredMovies
    {
        get { return _FilteredMovies; }
        set 
        {
            _FilteredMovies = value;

            //Have to implement property changed because in the sort method
            //I am instantiating a new observable collection.
            OnPropertyChanged("FilteredMovies");
        }
    }

    public SortChangedCommand SortChangedCommand { get; set; }

    public MoviesViewModel()
    {
        this.Movies = new ObservableCollection<Movie>();

        #region Test Data

        this.Movies.Add(new Movie()
        {
            Name = "Movie 1"
        });

        this.Movies.Add(new Movie()
        {
            Name = "Movie 2"
        });


        this.Movies.Add(new Movie()
        {
            Name = "Movie 3"
        });

        #endregion

        //Copy the movies list to the filtered movies list (this list is displayed on the UI)
        this.FilteredMovies = new ObservableCollection<Movie>(this.Movies);

        this.SortChangedCommand = new SortChangedCommand(this);
    }

    public void Sort(string sortOption)
    {
        switch (sortOption)
        {
            case "A-Z": this.FilteredMovies = new ObservableCollection<Movie>(this.Movies.OrderBy(x => x.Name)); break;
            case "Z-A": this.FilteredMovies = new ObservableCollection<Movie>(this.Movies.OrderByDescending(x => x.Name)); break;
        }
    }
}

The view model contains two lists, one to hold all movies, and the other to hold a list of filtered movies. The filtered movies will be displayed on the UI.

Here is the view:

<Window x:Class="BorderCommandExample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ViewModels="clr-namespace:BorderCommandExample"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <ViewModels:MoviesViewModel x:Key="ViewModel"/>
</Window.Resources>
<Grid DataContext="{StaticResource ViewModel}">
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <Button Command="{Binding SortChangedCommand}" CommandParameter="A-Z" 
            Content="A-Z"/>
    <Button Command="{Binding SortChangedCommand}" CommandParameter="Z-A" 
            Content="Z-A"
            Grid.Row="1"/>

    <ListBox ItemsSource="{Binding FilteredMovies}"
             DisplayMemberPath="Name"
             Grid.Row="2"/>
</Grid>

Note: I didn't use a ComboBox, as the ComboBox control doesn't have the Command properties, but you can easily get around this.

When you click on any of the buttons, the command will execute and call the sort method. And the UI will then update with the filtered movies.

The UI itself is probably not what you were after, however the use of a command to achieve this is what you need.

Community
  • 1
  • 1
Mike Eason
  • 9,525
  • 2
  • 38
  • 63
  • Wow this is great. Thanks for the example! I will go through it. –  Jan 07 '15 at 00:12
  • Great, let me know if you have any issues with it. – Mike Eason Jan 07 '15 at 08:18
  • What I don't like/understand about this solution is that it does not use the `CollectionViewSource` at all. `CollectionViewSource` is a major part of the WPF framework (although it's mostly hidden) and already supports sorting/filtering/grouping in a WPF-optimized way. I would prefer using `CollectionViewSource` (if nothing else, then to avoid re-inventing the wheel), but all examples I see are relying on code behind. – Robert F. Feb 05 '22 at 09:38