In this article, you will find the complete application with two different animation patterns implemented in two modules. The application is modular and could be easily extended by adding new modules.
Introduction
The post is devoted to screen saver application written in WPF with Prism pattern. The provided code is the complete application with two different animation patterns that are implemented in two modules. The application is modular and could be easily extended by adding new modules.
Features
The application demonstrates the following features:
- Can be installed as screen saver
- Supports secondary displays
- Contains two animation modules: blinking stripes and animated grid
- User may change modules on the fly
- User may restart the animation and change settings of the modules
- Settings are saved in XML files
- Logging with log4net library
Background
The solution uses C#6, .NET 4.5.1, WPF with Prism pattern, NuGet packages Extended.Wpf.Toolkit, Ikc5.Prism.Settings and Ikc5.TypeLibrary.
Screen Saver
Screen saver is an ordinary GUI application but with a special way of the launching. It should be able to accept input parameters and to execute in three modes: show, preview and configure.
There are some posts where WPF screen savers are described:
The using of Prism pattern allows to create a modular application where animated modules could be easily added and then switched on the fly. Views in modules implement IActiveAware
interface that allow to control activity of the view and execute commands. Modules and the WPF application has settings that include sizes, colors, iteration delays. The application uses Ikc5.Prism.Settings
packages, described in Examples of using Ikc5.Prism.Settings, and saves settings of the application and modules in XML files in %AppData% folder.
Solution
The solution has the following structure:
- Common -
Common.Models
contains enumerations, models classes and interfaces; Common.ViewModels
contains attached properties, hierarchy of converters, styles and view models - First module -
FirstModule.Models
contains settings class and model classes; FirstModule.Views
contains module class, views and view models - Second module - has the same structure as the first module
- ScreenSaver - the main WPF application
Common Class Libraries
Animation modules are based on dynamic grid described in Grid with dynamic number of rows and columns, part 2. That post describes WPF datagrid
with cells that have defined fixed size but number of rows and columns is updated dynamically in order to fill all available space. Here and below, we refer to classes from that code.
Common.Models
includes ICell
interface, Cell
and CellSet
classes. Cell
is not changed and has one Boolean property, and cell set gets additional method InvertPoint
for iterations:
public void InvertPoints(IEnumerable<Point> newPoints)
{
if (newPoints == null)
return;
foreach (var point in newPoints)
{
Cells[point.X, point.Y].State = !Cells[point.X, point.Y].State;
}
}
Common.Models
includes IDynamicGridViewModel
and IBaseCellViewModel
interfaces, design and base view models. IBaseCellViewModel
is used by a view that shows cell model, and could be inherited and extended in modules by additional properties like colors. It is simple:
public interface IBaseCellViewModel
{
ICell Cell { get; set; }
}
IDynamicGridViewModel
interface is extended by iterate
commands and IActiveAware
interface:
public interface IDynamicGridViewModel<TCellViewModel> :
IActiveAware where TCellViewModel : IBaseCellViewModel
{
int ViewWidth { get; set; }
int ViewHeight { get; set; }
int CellWidth { get; set; }
int CellHeight { get; set; }
int GridWidth { get; }
int GridHeight { get; }
CellSet CellSet { get; }
ObservableCollection<ObservableCollection<TCellViewModel>> Cells { get; }
ICommand IterateCommand { get; }
ICommand StartIteratingCommand { get; }
ICommand StopIteratingCommand { get; }
ICommand RestartCommand { get; }
}
Implementation of the interface is an abstract
DynamicGridViewModel
class. It keeps code from mentioned post, implements IActiveAware
and is extended by iteration methods and commands. Implementation and objectives of IActiveAware
interface is described in Detecting the Active View in a Prism App.
View model contains iteration timer:
_iterateTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(IterationDelay),
};
_iterateTimer.Tick += IterateTimerTick;
When timer ticks, IterateTimerTick
method is called, where randomly generated set of cells inverts states:
private void IterateTimerTick(object sender, EventArgs e)
{
Iterate();
}
private void Iterate()
{
if (CellSet == null)
return;
using (Application.Current.Dispatcher.DisableProcessing())
{
var points = GenerateRandomPoints(11);
CellSet.InvertPoints(points);
}
}
In addition, iteration timer should be paused and then continued if cell set is recreated due to change in view size. Therefore, class contains methods such that StartTimer
, StopTimer
, PauseIteration
and ContinueIteration
with natural implementation.
protected void PauseIteration()
{
if (_iterateTimer.IsEnabled)
{
_postponedTimer = true;
_iterateTimer.Stop();
}
}
protected void ContinueIteration()
{
if (_postponedTimer)
StartTimer();
}
private void StartTimer()
{
if (CellSet == null || Cells == null)
_postponedTimer = true;
else
{
_iterateTimer.Start();
_postponedTimer = false;
SetCommandProviderMode(CommandProviderMode.Iterating);
}
}
private void StopTimer()
{
_iterateTimer.Stop();
SetCommandProviderMode(CommandProviderMode.Init);
}
Modules
The application contains two modules. The first module shows grid with cells in two colors: StartColor
if State
equals true
, and FinishColor
otherwise. The second module shows vertical stripes with gradient fill that corresponds to cell models with State
equals true
. Animations are implemented by CellView
classes.
<Grid x:Name="MainPanel">
<Border
BorderThickness="1"
BorderBrush="{Binding BorderColor,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}},
Converter={StaticResource ColorToBrushConverter},
FallbackValue=#FF000000}"
Background="{Binding StartColor,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}},
Converter={StaticResource ColorToBrushConverter},
FallbackValue=#FF40FF40}">
<Border
BorderThickness="0"
Background="{Binding FinishColor,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}},
Converter={StaticResource ColorToBrushConverter},
FallbackValue=#FFFF4040}"
Visibility="{Binding Path=Cell.State, Mode=OneWay,
Converter={StaticResource BooleanToVisibilityConverter},
FallbackValue=Hidden}"
attached:VisibilityAnimation.AnimationType=
"{Binding Path=Settings.AnimationType, Mode=OneWay}"
attached:VisibilityAnimation.AnimationDuration=
"{Binding Path=Settings.AnimationDelay, Mode=OneWay}"/>
</Border>
</Grid>
<Grid x:Name="MainPanel">
<Border
BorderThickness="0"
Background="Transparent">
<Border
BorderThickness="1"
BorderBrush="{Binding BorderColor,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}},
Converter={StaticResource ColorToBrushConverter},
FallbackValue=#FF000000}"
Visibility="{Binding Path=Cell.State, Mode=OneWay,
Converter={StaticResource BooleanToVisibilityConverter},
FallbackValue=Visible}"
attached:VisibilityAnimation.AnimationType=
"{Binding Path=Settings.AnimationType, Mode=OneWay}"
attached:VisibilityAnimation.AnimationDuration=
"{Binding Path=Settings.AnimationDelay, Mode=OneWay}">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="{Binding StartColor,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}},
FallbackValue=#FF40FF40}" Offset="0"/>
<GradientStop Color="{Binding FinishColor,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}},
FallbackValue=#FFFF4040}" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
</Border>
</Grid>
Modules are constructed in a similar way, so let's consider one of them. FirstModule.Models
class library includes ISettings
interface that lists settings of the module: colors, sizes and delays, and default implementation of this interface - Settings
class. FirstModule.Views
contains MainView
, SettingsView
and CellView
views and corresponding view models. MainView
code is the same as described in the mentioned post. SettingsView
is used in a region of SettingsWindow
window of the application and provides to user current settings and possibility to change them.
IMainViewModel
and ICellViewModel
interfaces are derived from IDynamicGridViewModel
and IBaseCellViewModel
, respectively. Derived interfaces contains ISettings
instance that allows to form presentation based on module's settings.
Further, MainViewModel
view model are derived from DynamicGridViewModel
and just implement ISettings
property and subscriber to PropertyChanged
event.
private ISettings _settings;
public ISettings Settings
{
get { return _settings; }
private set
{
var userSettings = _settings as IUserSettings;
if (userSettings != null)
userSettings.PropertyChanged -= UserSettingsOnPropertyChanged;
SetProperty(ref _settings, value);
userSettings = _settings as IUserSettings;
if (userSettings != null)
userSettings.PropertyChanged += UserSettingsOnPropertyChanged;
}
}
private void UserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (CellSet == null)
return;
PauseIteration();
switch (args.PropertyName)
{
case nameof(Settings.CellWidth):
CellWidth = Settings.CellWidth;
break;
case nameof(Settings.CellHeight):
CellHeight = Settings.CellHeight;
break;
case nameof(Settings.IterationDelay):
IterationDelay = Settings.IterationDelay;
break;
default:
break;
}
ContinueIteration();
}
Interrelation Between Modules
One of the principles of Prism pattern is the independence of modules, but the application needs to interact with modules. In this example, the main window shows context menu that contains "Restart" item. It requires to find active view and to restart its animations. This task is solved by the using CompositeCommand
and DelegateCommand
.
Module main views implement IActiveAware
interface that is supported by Prism infrastructure. Then view passes calls to view model and commands, and view model is aware of the module's activity.
Common library contains ICommandProvider
interface:
public interface ICommandProvider : INotifyPropertyChanged
{
CompositeCommand IterateCommand { get; }
CompositeCommand StartIteratingCommand { get; }
CompositeCommand StopIteratingCommand { get; }
CompositeCommand RestartCommand { get; }
}
In the main application, this interface is implemented by CommandProvider
class, where composite commands are created with awareness of active modules:
public class CommandProvider : BindableBase, ICommandProvider
{
public CompositeCommand IterateCommand { get; }
= new CompositeCommand(monitorCommandActivity: true);
public CompositeCommand StartIteratingCommand { get; }
= new CompositeCommand(monitorCommandActivity: true);
public CompositeCommand StopIteratingCommand { get; }
= new CompositeCommand(monitorCommandActivity: true);
public CompositeCommand RestartCommand { get; }
= new CompositeCommand(monitorCommandActivity: true);
}
View model of the main view in each module creates delegate commands and registers them:
IterateCommand = new DelegateCommand(Iterate, () => CanIterate)
{ IsActive = IsActive };
StartIteratingCommand = new DelegateCommand(StartTimer, () => CanStartIterating)
{ IsActive = IsActive };
StopIteratingCommand = new DelegateCommand(StopTimer, () => CanStopIterating)
{ IsActive = IsActive };
RestartCommand = new DelegateCommand(Restart, () => CanRestart)
{ IsActive = IsActive };
commandProvider.IterateCommand.RegisterCommand(IterateCommand);
commandProvider.StartIteratingCommand.RegisterCommand(StartIteratingCommand);
commandProvider.StopIteratingCommand.RegisterCommand(StopIteratingCommand);
commandProvider.RestartCommand.RegisterCommand(RestartCommand);
CanExecute
properties are updated depending on different states of the iteration and the creating models.
Add New Module
As was mentioned above, new modules can be easily added. The application provides to user the list of all registered modules and allows to choose active one. Settings window contains tabs for settings of all registered modules. So let's consider steps to add module named ThirdModule
:
- Create new class libraries
ThirdLibrary.Views
and ThirdLibrary.Views
or copy SecondModule.*
libraries and rename all files and classes from "Second
" to "Third
". - Update properties in
ISettings
interface in order to correspond module settings; update all derived classes like Settings
, DesignSettings
, ISettingsViewModel
interface, SettingsViewModel
, and update elements and bindings in SettingsView
view. - Register module's views in application's regions:
public void Initialize()
{
_regionManager.RegisterViewWithRegion(PrismNames.MainRegionName, typeof(MainView));
_regionManager.RegisterViewWithRegion($"{GetType().Name}
{RegionNames.ModuleSettingsRegion}", typeof(SettingsView));
}
- Register module in
Bootstrapper
class:
protected override void ConfigureModuleCatalog()
{
var catalog = (ModuleCatalog)ModuleCatalog;
catalog.AddModule(typeof(FirstModule.FirstModule));
catalog.AddModule(typeof(SecondModule.SecondModule));
catalog.AddModule(typeof(ThirdModule.ThirdModule));
}
- If module animation is based on dynamic grid, there is enough to update
CellView
view; otherwise it is necessary to write code for MainView
and CellView
views.
Screen Saver Application
WPF application contains views, view models, bootstrapper and application classes. MainWindow
and EmptyWindow
are primary windows for screen saver. Settings
class and SettingsWindow
is described in Examples of using Ikc5.Prism.Settings. The main window has the main region that occupies the entire space and context menu.
<Grid x:Name="MainGrid"
d:DataContext="{d:DesignInstance Type=viewModels:DesignMainWindowModel,
IsDesignTimeCreatable=True}">
<Grid.Background>
<SolidColorBrush Color="{Binding Path=Settings.BackgroundColor,
Mode=OneWay, FallbackValue=#FFC0C0C0}"/>
</Grid.Background>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Restart"
Command="{Binding RestartCommand, Mode=OneWay}"
Click="MenuItem_OnClick"/>
<Separator />
<MenuItem Header="Settings"
Command="{Binding SettingsCommand, Mode=OneWay}"
Click="MenuItem_OnClick"/>
<MenuItem Header="About"
Command="{Binding AboutCommand, Mode=OneWay}"
Click="MenuItem_OnClick"/>
</ContextMenu>
</Grid.ContextMenu>
<ContentControl
regions:RegionManager.RegionName="MainRegion"/>
</Grid>
Context menu:
Restart
- restart current view with random set of active cells Settings
- show settings window and allow user change settings without stopping screen saver About
- show about dialog
According to Prism pattern, the application uses Bootstrapper
class, where all necessary initialization is executed, and OnStartup
method usually looks in the following way:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var bootstrapper = new Bootstrapper();
bootstrapper.Run();
}
For screen saver application, the following steps should be done in OnStartup
method:
- Create bootstrapper, but don't show the main window
- Set
Shutdown
mode according to launch type of screen saver - Parse input arguments and choose the necessary behavior of the application
- In the show mode, create additional windows for secondary monitors
- In the setting mode, show setting window and correct shutdown mode
- In the preview mode, show WPF window in Win32 window and take care of resource clean-up
Below, we consider implementation of these steps.
Initialization
Main window is called Shell
in Prism, and usually created in CreateShell
method and initialized in InitializeShell
method:
protected override DependencyObject CreateShell()
{
Window mainWindow = Container.Resolve<MainWindow>();
return mainWindow;
}
protected override void InitializeShell()
{
var regionManager = Container.Resolve<IRegionManager>();
Application.Current.MainWindow.Show();
}
In the screen saver, the main window should be completely created, but stays hidden as screen saver could be launched in preview or configure mode. Therefore, InitializeShell
doesn't show the main window:
protected override void InitializeShell()
{
var regionManager = Container.Resolve<IRegionManager>();
}
If application runs in show mode, it just shows main window in OnStartup
mode:
Application.Current.MainWindow.Show();
Input Arguments
There are the following command-line parameters all screen savers need to handle:
/s
– show the screensaver /p
– preview the screensaver /c
– configure the screensaver
In addition, arguments could be separated by colon, for examples: /c:1234567
or /P:1234567
. The application uses the following enumeration for input arguments:
public enum LaunchType
{
[Description("No parameters")]
Default = 0,
[Description("\\s, Show the screen saver")]
Show,
[Description("\\c, Configure settings")]
Configure,
[Description("\\p, Show in preview mode")]
Preview
}
Input arguments are lowered, split by colon, and compare with expected string
s. As results, two variables are set: launch type and window descriptor, that is used in preview mode.
var launchType = LaunchType.Default;
var previewWindowDescriptor = 0;
logger.Log($"Start parameters: {string.Join("; ", e.Args)}", Category.Info);
if (e.Args.Length > 0)
{
var firstArgument = e.Args[0].ToLower().Trim();
string secondArgument = null;
if (firstArgument.Length > 2)
{
secondArgument = firstArgument.Substring(3).Trim();
firstArgument = firstArgument.Substring(0, 2);
}
else if (e.Args.Length > 1)
secondArgument = e.Args[1];
if (string.Equals("/c", firstArgument))
launchType = LaunchType.Configure;
else if (string.Equals("/s", firstArgument))
launchType = LaunchType.Show;
else if (string.Equals("/p", firstArgument))
launchType = LaunchType.Preview;
if (!string.IsNullOrEmpty(secondArgument))
previewWindowDescriptor = Convert.ToInt32(secondArgument);
}
logger.Log($"Converted start parameters: launchType={launchType},
previewWindowDescriptor={previewWindowDescriptor}");
Then, the switch is used for providing different behavior depending on launch type.
Show the Screen Saver
As main window is already created, it could be shown. But there is an issue if computer has several displays. By default, operation system blacks all other displays except primary, so it is enough to show main window on primary screen. Depending on application settings, user would like to show screen saver on all displays. The post WPF windows on two screens shows how to position WPF window on secondary monitor or show two windows on two monitors.
Therefore, the application shows main window at primary display, for all secondary displays creates and positions EmptyWindow
or new instance of MainWindow
. Then Shutdown
mode is set to default value:
Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
Configure the Screensaver
In this mode, application shows settings window. As the application uses Ikc5.Prism.Settings packages, it contains SettingsWindow
that allows user to set settings and options of the application. But the main window won't be shown and should be closed without shutdown of the application. On the other hand, if the main window will not be closed, application continues to execute in the background. That is why the application shows SettingsWindow
, sets shutdown mode to ShutdownMode.OnLastWindowClose
and then closes the main window. When user closes settings window, it is considered as the last window in application, and application exits.
var settingsWindow = bootstrapper.Container.Resolve<SettingsWindow>();
settingsWindow.Show();
Current.ShutdownMode = ShutdownMode.OnLastWindowClose;
Current.MainWindow.Close();
Preview the Screen Saver
In this mode, screen saver is displayed in small window. This mode requires that the application should adapt to small size of screen, and WPF window is shown in Win32 window. Another issue is that it is necessary to catch event when parent window is disposed, and close the application. Otherwise, the current instance of the application continues to execute in background. Necessary code is covered by the above mentioned posts about screen saver in WPF, so there is a slightly brushed code:
var mainWindow = Current.MainWindow as MainWindow;
if (mainWindow == null)
{
Current.Shutdown();
return;
}
logger.Log("Init objects for preview mode");
var pPreviewHandle = new IntPtr(previewWindowDescriptor);
var lpRect = new RECT();
var bGetRect = Win32API.GetClientRect(pPreviewHandle, ref lpRect);
var sourceParams = new HwndSourceParameters("sourceParams")
{
PositionX = 0,
PositionY = 0,
Width = lpRect.Right - lpRect.Left,
Height = lpRect.Bottom - lpRect.Top,
ParentWindow = pPreviewHandle,
WindowStyle = (int)(WindowStyles.WS_VISIBLE |
WindowStyles.WS_CHILD | WindowStyles.WS_CLIPCHILDREN)
};
logger.Log($"Source param size = ({0}, {0}, {lpRect.Right - lpRect.Left},
{lpRect.Bottom - lpRect.Top})");
_winWpfContent = new HwndSource(sourceParams)
{
RootVisual = mainWindow.MainGrid
};
_winWpfContent.Disposed += (o, args) =>
{
logger.Log("_winWpfContent is Disposed, close main window and application");
mainWindow.Close();
Current.Shutdown();
};
logger.Log(
$"MainWindow is shown in preview, IsVisible={mainWindow.IsVisible},
IsActive={mainWindow.IsActive}, Owner={mainWindow.Owner?.Title}" +
$", Rect=({mainWindow.Left}, {mainWindow.Top},
{mainWindow.Width}, {mainWindow.Height})");
Install the Screen Saver
The solution contains Publish
configuration, that renames executable file to Ikc5.ScreenSaver.scr in post-build steps:
if $(ConfigurationName) NEQ Publish Exit 0
cd "$(TargetDir)"
del "$(TargetName).scr"
del "$(TargetName).scr.config"
ren "$(TargetFileName)" "$(TargetName).scr"
ren "$(TargetFileName).config" "$(TargetName).scr.config"
In order to install screen saver, it is necessary to call context menu for Ikc5 ScreenSaver.scr file in File Explorer, and then click on Install
item. Screen Saver Window allows to set timeout, settings of screen saver, and preview it. Images below show these steps.
History
- 28th January, 2017 - Initial post
- 29th January, 2017 - Updated zip files (includes fixes from GIT repository)