Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional settings in WPF application

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.

like image 705
Deadzone Avatar asked Feb 07 '18 13:02

Deadzone


3 Answers

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.

like image 77
Gaurang Dave Avatar answered Oct 11 '22 11:10

Gaurang Dave


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
like image 28
Isma Avatar answered Oct 11 '22 11:10

Isma


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.

like image 35
Steve Harris Avatar answered Oct 11 '22 11:10

Steve Harris