5

I'm making user changeable settings for my media player and I'm struggling to find an elegant solution to the problem.

One of my settings for example - pauses the video at it's last frame, if not checked it will either continue through the playlist or if it's only 1 file, reset it and pause it at the start.

This is how I've implemented it:

private void OnMediaEndedCommand()
{
    if (GeneralSettings.PauseOnLastFrame)
    {
        MediaPlayer.SetMediaState(MediaPlayerStates.Pause);
        return;
    }
    if (PlayListViewModel.FilesCollection.Last().Equals(PlayListViewModel.FilesCollection.Current) && !Repeat)
    {
        ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
        MediaPlayer.SetMediaState(MediaPlayerStates.Stop);
        return;
    }
    ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
}

This is contained inside the ViewModel of the main window, where the media element is and GeneralSettings.PauseOnLastFrame is a boolean property.

This command is binded as follows:

<MediaElement ....>
    <ia:Interaction.Triggers>
        <ia:EventTrigger EventName="MediaEnded">
            <ia:InvokeCommandAction Command="{Binding MediaEndedCommand}"/>
        </ia:EventTrigger>
    </ia:Interaction.Triggers>
</MediaElement>

It works but it's awful, how should I go about implementing such setting system in an elegant way? Some settings might not be boolean, they might have multiple options, some might be applied only on startup and others, as the one illustrated above, event based.

Deadzone
  • 793
  • 1
  • 11
  • 33
  • If there is a property for the control which you can bind to settings, you can use data-binding, otherwise you need to write code to use settings. As an option, you can use Settings classes which are based on `System.Configuration.ApplicationSettingsBase`. They support two-way data-binding and you can simply edit them and use them in data-binding. You can also create setting classes in code or using designer. – Reza Aghaei Feb 10 '18 at 06:55
  • Why the downvote? – Deadzone Feb 10 '18 at 15:59

3 Answers3

5

Based on the information and sample code you provided, I would suggest

Approach - 1

A tightly couple ViewModel with System.Configuration.ApplicationSettingsBase and you can mention all you properties in ViewModel and map single of them with a separate application setting property. You can use your settings directly in biding afterwards e.g. : {x:Static Settings.Default.Whatevs}. Othe "Save" button click event or main window close event, you can save all you settings e.g. : Settings.Default.Save();

Approach - 2

A better approach, I would suggest / prefer (if I am developing this app) is to develop a wrapper class (e.g.: SettingProvider) that implement an inheritance (e.g: ISettingProvider) which uncovers all you settings as separate properties and also have a save method which saves all setting values. You can use this wrapper class into your ViewModel to handle all the commands and setting values in better way.

The benefit of this approach is the if you decide to change you setting to database , you need not to make change to you ViewModel as all job is done in SettingProvider class.

I am not sure but based on viewing your code, I assume that you used Approach-1. Please put you comments and any feedback to this answer. I would like to know what you think and may be you have got more simple and interesting way of achieving this.

UPDATE-1

Example

Enum for showing you demo

public enum MediaStatus
{
    Playing = 0,
    Stopped = 1,
    Paused = 2
}

Interface

public interface ISettingProvider
{
    double Volumne { get; set; }
    string LastMediaUrl { get; set; }
    MediaStatus PlayingMediaStatus;

    void SaveSettings();
}

Wrapper Class

public class SettingProvider : ISettingProvider
{
    private double volumne;
    public double Volumne  // read-write instance property
    {
        get
        {
            return volumne;
        }
        set
        {
            volumne = value;
            Settings.Default.Volumne = volumne;
        }
    }

    private string lastMediaUrl;
    public string LastMediaUrl  // read-write instance property
    {
        get
        {
            return lastMediaUrl;
        }
        set
        {
            lastMediaUrl = value;
            Settings.Default.LastMediaUrl = lastMediaUrl;
        }
    }

    private MediaStatus playingMediaStatus;
    public MediaStatus PlayingMediaStatus  // read-write instance property
    {
        get
        {
            return playingMediaStatus;
        }
        set
        {
            playingMediaStatus = value;
            Settings.Default.PlayingMediaStatus = (int)playingMediaStatus;
        }
    }

    public void SaveSettings()
    {
        Settings.Default.Save();
    }

    //Constructor
    public SettingProvider()
    {
        this.Volumne = Settings.Default.Volumne;
        this.LastMediaUrl = Settings.Default.LastMediaUrl;
        this.PlayingMediaStatus = (MediaStatus)Settings.Default.PlayingMediaStatus;

    }
}

ViewModelBase Class

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propName)
    {
        if (PropertyChanged != null)
        {
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }
}

CommandHandler Class

public class CommandHandler : ICommand
{
    public event EventHandler CanExecuteChanged { add { } remove { } }

    private Action<object> action;
    private bool canExecute;

    public CommandHandler(Action<object> action, bool canExecute)
    {
        this.action = action;
        this.canExecute = canExecute;
    }

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

    public void Execute(object parameter)
    {
        action(parameter);
    }
}

ViewModel

public class SettingsViewModel : ViewModelBase
{
    SettingProvider objSettingProvider = new SettingProvider();

    public double Volumne 
    {
        get
        {
            return objSettingProvider.Volumne;
        }
        set
        {
            objSettingProvider.Volumne = value;
            OnPropertyChanged("Volumne");
        }
    }

    // Implementaion of other properties of SettingProvider with your ViewModel properties;


    private ICommand saveSettingButtonCommand;
    public ICommand SaveSettingButtonCommand
    {
        get
        {
            return saveSettingButtonCommand ?? (saveSettingButtonCommand = new CommandHandler(param => saveSettings(param), true));
        }
    }

    private void saveSettings()
    {
        objSettingProvider.SaveSettings();
    }
}

UPDATE-2

public interface ISettingProvider
{
    bool PauseOnLastFrame;
    bool IsAutoPlay;
    MediaStatus PlayingMediaStatus;

    void SaveSettings();
}


public class SettingProvider : ISettingProvider
{
    private bool pauseOnLastFrame;
    public bool PauseOnLastFrame  // read-write instance property
    {
        get
        {
            return pauseOnLastFrame;
        }
        set
        {
            pauseOnLastFrame = value;
            Settings.Default.PauseOnLastFrame = volumne;
        }
    }

    private bool isAutoPlay;
    public bool IsAutoPlay  // read-write instance property
    {
        get
        {
            return isAutoPlay;
        }
        set
        {
            isAutoPlay = value;
            Settings.Default.IsAutoPlay = volumne;
        }
    }
}

public class SettingsViewModel : ViewModelBase
{
    SettingProvider objSettingProvider = new SettingProvider();


    MediaStatus PlayingMediaStatus 
    {
        get
        {
            return objSettingProvider.PlayingMediaStatus;
        }
        set
        {
            if(value == MediaStatus.Paused)
                MediaPlayer.Pause();

            if(value == MediaStatus.Playing)
                MediaPlayer.Play();

            if(value == MediaStatus.Stopped)
                MediaPlayer.Stop();

            objSettingProvider.PlayingMediaStatus  = (int)value;

            OnPropertyChanged("PlayingMediaStatus");
        }
    }

    private string currentMediaFile;
    public string CurrentMediaFile
    {
        get
        {
            return currentMediaFile;
        }
        set
        {
            currentMediaFile  = value;

            MediaPlayer.Stop();
            MediaPlayer.Current = currentMediaFile;

            if(objSettingProvider.IsAutoPlay)
                MediaPlayer.Play();

            OnPropertyChanged("CurrentMediaFile");
        }
    }

    // Implementaion of other properties of SettingProvider with your ViewModel properties;


    private ICommand onMediaEndedCommand;
    public ICommand OnMediaEndedCommand
    {
        get
        {
            return onMediaEndedCommand ?? (onMediaEndedCommand = new CommandHandler(param => onMediaEnded(param), true));
        }
    }

    private void onMediaEnded()
    {
        if(objSettingProvider.PauseOnLastFrame)
        {
            PlayingMediaStatus = MediaStatus.Paused;
        }

        else if(PlayListViewModel.FilesCollection.Last().Equals(PlayListViewModel.FilesCollection.Current) && !Repeat)
        {
            PlayingMediaStatus = MediaStatus.Stopped;
        }

        else
        {
            CurrentMediaFile = PlayListViewModel.FilesCollection.MoveNext();
        }
    }
}

NOTE: This is the detailed example I put here and also avoid some syntax error or naming error if I missed somewhere. Please correct it. I am not aware which media player settings you are using. I took some sample properties. This is just an example of structure you can implement for you application. You may need to alter more code to implement this structure.

Gaurang Dave
  • 3,956
  • 2
  • 15
  • 34
  • Thank you for your response, If I'm understanding correctly, second approach suggests that you will implement the setting in a separate file. I'm interested to see an example of that, as that was my initial idea, tho it had a few problems with it. – Deadzone Feb 12 '18 at 07:56
  • 1
    I will surely provide a sample demo class. Allow me some time. – Gaurang Dave Feb 12 '18 at 09:17
  • @Deadzone check mu updated answer. I put a detailed example with implementation of different data types and also provided some helper classes for command and viewmodel. Put you comment here if any. I am not sure about the media player settings you are using. I took some sample properties. – Gaurang Dave Feb 13 '18 at 01:31
  • Please provide an example code of the setting, I've illustrated in my question, pausing the video at the last frame if the setting is checked, if not the code in my question should be executed. – Deadzone Feb 13 '18 at 09:18
  • @Deadzone please check update-2 in answer. Also consider some limitation of mine that I could not take all the viewmodels you used or the actual properties you took. I am providing a code structure to you to better manager you code and flow. – Gaurang Dave Feb 14 '18 at 01:27
  • I'm sorry but I don't see any improvement, OnMediaEnded is still clustered with if/elses and we also have some superfluous classes. – Deadzone Feb 14 '18 at 13:29
  • Do you want me to design a solution for you? At any point you need to mention your conditions (if...else). Either in viewmodel or in wrapper class. But better is to do in viewmodel as wrapper class implementation , you can change based on requirement you have. Its ok if it is not fit to you need but I target was to provide you a structure which can be easily managable. Thanks :) – Gaurang Dave Feb 15 '18 at 00:51
4

An elegant way to implement this IMHO would be to use a dependency injection container, this will provide great flexibility while allowing you to completely separate concerns (i.e. the settings implementation from your view models and custom controls).

There are many DI frameworks out there, for my example I will use simple injector because it is free (open source), simple and fast but you can apply the same principle to other frameworks (Unity, Ninject, etc..).

Start by creating an interface for your settings service, for example:

public interface ISettingsService
{
    double Volumne { get; set; }
    string LastMediaUrl { get; set; }
    MediaStatus PlayingMediaStatus;

    void SaveSettings();
}

Then add your implementation for the service, the beauty of using DI is that you can change the implementation at anytime or completely replace it and your application will continue to work as usual.

Let's say you want to use application settings, here is your service:

public class SettingsServiceFromApplication : ISettingsService
{
    public double Volume 
    {
        get 
        {
           return Properties.Settings.Volume;
        }
    }
    [...]
}

Or let's say you want to use a database to store your settings:

public class SettingsServiceFromDb : ISettingsService
{
    public double Volume 
    {
        get 
        {
           return MyDb.Volumen;
        }
    }
    [...]
}

Then you can use a DI container to specify which implementation to use:

Start by installing the library using NuGet:

Install-Package SimpleInjector -Version 4.0.12 

You need a way to share your container throughout the application, I usually just go with a static class that I initialize when starting the app:

using Container = SimpleInjector.Container;

namespace YourNamespace
{
    public class Bootstrapper
    {
        internal static Container Container;

    public static void Setup()
    {
        //Create container and register services
        Container = new Container();

        //Let's specify that we want to use SettingsServiceFromApplication
        Container.Register<ISettingsService, SettingsServiceFromApplication>();

        //You can use your bootstrapper class to initialize other stuff
    }
}

You need to call Setup when starting the App, the best place is in the App constructor:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        Bootstrapper.Setup();
    }
}

So now you have an app wide depedency injection container that you can use to request "services" (specific implementations of an interface).

To get the settings implementation in your view models you could simply call the container as follows:

// This will return an instance of SettingsServiceFromApplication
ISettingsService settingsService = Bootstrapper.Container.GetInstance<ISettingsService>();
double volumen = settingsService.Volume;

To make it easier to work with, I usually create a base view model that will allow to get services more easyly, for example:

public abstract BaseViewModel
{
    private ISettingsService _settings;

    protected ISettingsService GeneralSettings
    {
         get
         {
             if (_settings == null)
                 _settings = Bootstrapper.Container.GetInstance<ISettingsService>();

             return _settings;
         }
    }
}

Every view model inheriting from this class will have access to the settings:

public class YourViewModel : BaseViewModel
{

    private void OnMediaEndedCommand()
    {
        if (GeneralSettings.PauseOnLastFrame)
        {
            MediaPlayer.SetMediaState(MediaPlayerStates.Pause);
            return;
        }
        if (PlayListViewModel.FilesCollection.Last().Equals(PlayListViewModel.FilesCollection.Current) && !Repeat)
        {
            ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
            MediaPlayer.SetMediaState(MediaPlayerStates.Stop);
            return;
        }
        ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
    }
}

As you can see the code is the same as your code! But now the settings are coming from your container. Where is the elegance? Well, let's say that one year from now someone decides that you will store your settings in a database, what do you need to change in your code?

Container.Register<ISettingsService, SettingsServiceFromDb>();

A single line. Everything else should work as usual.

As well as view models, you could use this mechanism in your own controls:

 public class MyMediaElement : UserControl //Or MediaElement and instead of commands you can override real events in the control code behind, this does not break the MVVM pattern at all, just make sure you use depedency properties if you need to exchange data with your view models
 {
     private void OnMediaEndedCommand()
     {
        //Get your settings from your container, do whatever you want to do depending on the settings
       [...]
     }
 }

Then just use your control in your Views / ViewModels:

<local:MyMediaElement />

Yep, that's all you need because you handle everything in your User / Custom control, your view models doesn't need to care about how you handle settings in the control.

There are many options you can use to register containers, I recommend you take a look at the docs:

https://simpleinjector.org/index.html
https://simpleinjector.readthedocs.io/en/latest/index.html
Isma
  • 14,604
  • 5
  • 37
  • 51
2

I think maybe you are looking for an interface approach?

public interface IMediaEndedHandler
{
    bool AlternateHandling(MediaPlayer player);
}
public class NullMediaEndedHandler : IMediaEndedHandler
{
    public bool AlternateHandling(MediaPlayer player)
    {
        return false;
    }
}
public class PauseOnLastFrameHandler : IMediaEndedHandler
{
    public bool AlternateHandling(MediaPlayer player)
    {
        player.SetMediaState(MediaPlayerStates.Pause);
        return true;
    }
}

public class GeneralSettings
{
    private bool pauseOnLastFrame;
    private bool PauseOnLastFrame
    {
        get
        {
            return pauseOnLastFrame;
        }
        set
        {
            pauseOnLastFrame = value;
            MediaEndedHandler = value
                ? new PauseOnLastFrameHandler()
                : new NullMediaEndedHandler();
        }
    }
    public IMediaEndedHandler MediaEndedHandler = new NullMediaEndedHandler();
}

Then:

private void OnMediaEndedCommand()
{
    if (GeneralSettings.MediaEndedHandler.AlternateHandling(MediaPlayer))
        return;

    if (PlayListViewModel.FilesCollection.Last().Equals(PlayListViewModel.FilesCollection.Current) && !Repeat)
    {
        ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
        MediaPlayer.SetMediaState(MediaPlayerStates.Stop);
        return;
    }
    ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
}

This way, if your setting is, for example. an enum instead of a bool, you can specify a different implementation of the interface for each possible value.

Steve Harris
  • 5,014
  • 1
  • 10
  • 25