Conditional settings in a WPF application

I make custom mutable settings for my media player, and I try my best to find an elegant solution to the problem.

For example, one of my settings - pauses the video on the last frame, if it is not set, it will either continue to play the playlist, or only 1 file, reset and pause it at the beginning.

Here's how I 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 within the ViewModel of the main window, where the media element and GeneralSettings.PauseOnLastFrame is a logical property.

This command is bound 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 use this tuning system in an elegant way? Some settings may not be logical, they may have several parameters, some can be applied only at startup, and others, as shown above, based on events.

+5
source share
3 answers

An elegant way to implement this IMHO is to use a container for dependency injection, which will provide greater flexibility, allowing you to completely separate problems (for example, implementing settings from view models and user controls).

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

Start by creating an interface for the 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 any time or completely replace it, and your application will continue to work as usual.

Suppose you want to use the application settings, here is your service:

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

Or say you want to use the database to store your settings:

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

Then you can use the DI container to indicate 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, usually I just run a static class that I initialize when the application starts:

 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 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 the installer when the application starts, the best place in the application constructor:

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

So now you have an injection container with an application extension that you can use to query for β€œservices” (specific implementations of the interface).

To get the implementation of settings in view models, you can simply call the container as follows:

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

To simplify the work, I usually create a basic presentation model that will make it easier to get services, for example:

 public abstract BaseViewModel { private ISettingsService _settings; protected ISettingsService GeneralSettings { get { if (_settings == null) _settings = Bootstrapper.Container.GetInstance<ISettingsService>(); return _settings; } } } 

Each 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 come from your container. Where is the elegance? Well, tell me that in a year someone will decide that you will save your settings in the database, what do you need to change in your code?

 Container.Register<ISettingsService, SettingsServiceFromDb>(); 

One line. Everything else should work as usual.

Like view models, you can use this mechanism in your own control:

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

Yes, all you need is because you process everything in your user / user control, you do not need to care about how you handle the settings in the control.

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

 https://simpleinjector.org/index.html https://simpleinjector.readthedocs.io/en/latest/index.html 
+4
source

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

Approach - 1

A dense pair of ViewModel with System.Configuration.ApplicationSettingsBase , and you can specify all the properties in the ViewModel and display one of them with a separate application settings property. You can use your settings directly while waiting later, for example: {x:Static Settings.Default.Whatevs} . In the "Save" field, click the "Event" button or the closing event of the main window, you can save all settings, for example: Settings.Default.Save();

Approach - 2

The best approach I would suggest / prefer (if I am developing this application) is to develop a wrapper class (for example: SettingProvider ) that implements inheritance (for example: ISettingProvider ) that exposes all your settings as well as have a save method that saves everything setting values. You can use this wrapper class in your ViewModel to better handle all commands and set values.

The advantage of this approach is that if you decide to change the settings in the database, you do not need to make changes to the ViewModel, since all tasks are performed in the SettingProvider class.

I'm not sure, but based on looking at your code, I assume you used Approach-1 . Please post comments and comments on this answer. I would like to know what you think and maybe you have a simpler and more interesting way to achieve this.

UPDATE-1

Example

Listing to demonstrate the 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(); } 

Class wrapper

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

Class ViewModelBase

 public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propName)); } } } 

Class commandhandler

 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. . Here is a detailed example that I gave here, and also avoid some syntax error or naming errors if I missed somewhere. Please fix this. I do not know what media player settings you use. I took some sample properties. This is just an example of the structure that you can implement for your application. You may need to modify the code to implement this structure.

+5
source

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

Thus, if your setting, for example. enum instead of bool, you can specify a different interface implementation for each possible value.

+2
source

Source: https://habr.com/ru/post/1275213/


All Articles