Skip to content

Easy localization, theme switching and other for WPF and Avalonia applications.

License

Notifications You must be signed in to change notification settings

CrackAndDie/Hypocrite.Services

Repository files navigation

Abdrakov.Solutions logo

Hypocrite.Services

Nuget Nuget License

About:

A package that helps You to create a powerful, flexible and loosely coupled WPF application. It fully supports Prism features and MVVM pattern.

Download:

.NET CLI:

dotnet add package Hypocrite.Services

Package Reference:

<PackageReference Include="Hypocrite.Services" Version="*" />

Demo:

Demo could be downloaded from releases

image1 image2

Getting started:

Topics:

First steps:

When you created your WPF app you should rewrite your App.xaml and App.xaml.cs files as follows:

<engine:ApplicationBase x:Class="YourNamespace.App"
                            xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
                            xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
                            xmlns:local="clr-namespace:YourNamespace"
                            xmlns:engine="clr-namespace:Abdrakov.Engine.MVVM;assembly=Abdrakov.Engine">
</engine:ApplicationBase>
namespace YourNamespace
{
    public partial class App : AbdrakovApplication
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
        }

        protected override Window CreateShell()
        {
            var viewModelService = Container.Resolve<IViewModelResolverService>();
            viewModelService.RegisterViewModelAssembly(Assembly.GetExecutingAssembly());

            return base.CreateShell();
        }

        protected override void OnExit(ExitEventArgs e)
        {
            base.OnExit(e);
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            base.RegisterTypes(containerRegistry);
            containerRegistry.RegisterInstance(new BaseWindowSettings()
            {
                ProductName = "Abdrakov.Demo",
                LogoImage = "pack:https://application:,,,/Abdrakov.Tests;component/Resources/AbdrakovSolutions.png",
            });
            containerRegistry.RegisterSingleton<IBaseWindow, MainWindowView>();
        }

        protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
        {
            base.ConfigureModuleCatalog(moduleCatalog);
            // ...
        }
    }
}

As you can see there is a BaseWindowSettings registration that gives you quite flexible setting of your Window (in this example we use MainWindowView from Abdrakov.CommonWPF namespace). Here you can see all the available settings of the Window:

  • LogoImage - Path to the logo image of your app like "pack:https://application:,,,/Abdrakov.Tests;component/Resources/AbdrakovSolutions.png"
  • ProductName - Title text that will be shown on the window header
  • MinimizeButtonVisibility - How should be Minimize button be shown (default is Visible)
  • MaxResButtonsVisibility - How should be Maximize and Restore buttons be shown (default is Visible)
  • WindowProgressVisibility - How should be Progress state be shown (default is Visible)
  • SmoothAppear - How sould be window appeared (true - smooth, false - as usual)

You can use Regions.MAIN_REGION to navigate in the MainWindowView:

internal class MainModule : IModule
{
    public void OnInitialized(IContainerProvider containerProvider)
    {
        var region = containerProvider.Resolve<IRegionManager>();
        // the view to display on start up
        region.RegisterViewWithRegion(Regions.MAIN_REGION, typeof(MainPageView));
    }

    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterForNavigation<MainPageView>();
    }
}

Preview window:

You can create your own preview window using Abdrakov.Solutions. Your preview window has to implement IPreviewWindow interface. Here is the sample preview window that shows up for 4 seconds:

public partial class PreviewWindowView : Window, IPreviewWindow
{
    private DispatcherTimer timer;
    public PreviewWindowView()
    {
        InitializeComponent();

        timer = new DispatcherTimer()
        {
            Interval = TimeSpan.FromSeconds(4),
        };
        timer.Tick += (s, a) => { CallPreviewDoneEvent(); };
        timer.Start();
    }

    public void CallPreviewDoneEvent()
    {
        timer.Stop();
        var cont = (Application.Current as AbdrakovApplication).Container;
        cont.Resolve<IEventAggregator>().GetEvent<PreviewDoneEvent>().Publish();
        this.Close();
    }
}

Your preview window should be also registered like this:

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    base.RegisterTypes(containerRegistry);

    containerRegistry.RegisterSingleton<IPreviewWindow, PreviewWindowView>();

    // ...
}

Registered Window under IBaseWindow interface will be shown up after PreviewDoneEvent event call.

Theme registrations:

Abdrakov.Solutions supports realtime theme change and flexible object registrations. Here is the sample code to register ThemeSwitcherService in your App (if you don't want the theme change service in your project you can skip this registration):

containerRegistry.RegisterInstance(new ThemeSwitcherService<Themes>()
{
    NameOfDictionary = "ThemeHolder",
    ThemeSources = new Dictionary<Themes, string>()
    {
        { Themes.Dark, "/Abdrakov.Demo;component/Resources/Themes/DarkTheme.xaml" },
        { Themes.Light, "/Abdrakov.Demo;component/Resources/Themes/LightTheme.xaml" },
    },
});

In my app Themes is an enum of themes for the app:

public enum Themes
{
    Dark,
    Light
}

Using the ThemeSwitcherService you can change an App's theme in realtime using method ChangeTheme(theme) and get current theme using method GetCurrentTheme().

For proper work of ThemeSwitcherService You should create ResourceDictionaries for each theme You have and a ResourceDictionary that will hold all the changes of themes. So in my project I created DarkTheme.xaml, LightTheme.xaml and ThemeHolder.xaml.
DarkTheme.xaml:

<ResourceDictionary xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
    <Color x:Key="TextForegroundBrushColor">AliceBlue</Color>
    <SolidColorBrush x:Key="TextForegroundBrush" 
                     Color="{DynamicResource TextForegroundBrushColor}"/>

    <Color x:Key="WindowStatusBrushColor">#2f80ed</Color>
    <SolidColorBrush x:Key="WindowStatusBrush"
                     Color="{DynamicResource WindowStatusBrushColor}" />

    <Color x:Key="WindowBrushColor">#070c13</Color>
    <SolidColorBrush x:Key="WindowBrush"
                     Color="{DynamicResource WindowBrushColor}" />
</ResourceDictionary>

LightTheme.xaml:

<ResourceDictionary xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
    <Color x:Key="TextForegroundBrushColor">Black</Color>
    <SolidColorBrush x:Key="TextForegroundBrush"
                     Color="{DynamicResource TextForegroundBrushColor}" />
    
    <Color x:Key="WindowStatusBrushColor">#2f80ed</Color>
    <SolidColorBrush x:Key="WindowStatusBrush"
                     Color="{DynamicResource WindowStatusBrushColor}" />
    
    <Color x:Key="WindowBrushColor">#fefefe</Color>
    <SolidColorBrush x:Key="WindowBrush"
                     Color="{DynamicResource WindowBrushColor}" />
</ResourceDictionary>

ThemeHolder.xaml (You should set here a default theme):

<ResourceDictionary xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/Abdrakov.Demo;component/Resources/Themes/DarkTheme.xaml"/>
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

Here are the colors and brushes that should be registered in your app:

  • TextForegroundBrush(Color) - The colors of the product name and window buttons
  • WindowStatusBrush(Color) - The colors of the window progress indicator
  • WindowBrush(Color) - The colors of the window and window header backgrounds

The holder of .xaml theme files (ThemeHolder.xaml) should be merged into Your app resources like this:

<engine:AbdrakovApplication x:Class="Abdrakov.Demo.App"
                            xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
                            xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
                            xmlns:local="clr-namespace:Abdrakov.Demo"
                            xmlns:engine="clr-namespace:Abdrakov.Engine.MVVM;assembly=Abdrakov.Engine">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Abdrakov.Demo;component/Resources/Themes/ThemeHolder.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</engine:AbdrakovApplication>

Registered colors and brushes could be used as DynamicResources like that:

<Rectangle Fill="{DynamicResource TestBrush}"/>

There is also a ThemeChangedEvent event that is called through IEventAggregator with ThemeChangedEventArgs. You can subscribe like this (IEventAggregator is already resolved in ViewModelBase):

(Application.Current as AbdrakovApplication).Container.Resolve<IEventAggregator>().GetEvent<ThemeChangedEvent>().Subscribe(YourMethod);

Localization:

Abdrakov.Solutions also provides you a great realtime localization solution. To use it you should create a folder Localization in your project and make some changes in your App.xaml.cs file:

public partial class App : AbdrakovApplication
{
    protected override void OnStartup(StartupEventArgs e)
    {
        LocalizationManager.InitializeExternal(Assembly.GetExecutingAssembly(), new ObservableCollection<Language>()
        {
            new Language() { Name = "EN" },
            new Language() { Name = "RU" },
        });  // initialization of LocalizationManager static service
        // ...
        base.OnStartup(e);
    }
}

In the Localization folder you create Resource files with translations and call it as follows - "FileName"."Language".resx (Gui.resx or Gui.ru.resx). Default resource file doesn't need to have the "Language" part.

Now you can use it like this:

<TextBlock Text="{LocalizedResource MainPage.TestText}"
            Foreground="{DynamicResource TextForegroundBrush}" />

Or via Bindings:

<TextBlock Text="{LocalizedResource {Binding TestText}}"
            Foreground="{DynamicResource TextForegroundBrush}" />

(I have this in my .resx files):
image image

To change current localization you can change LocalizationManager.CurrentLanguage property like this:

LocalizationManager.CurrentLanguage = CultureInfo.GetCultureInfo(lang.Name.ToLower());

In this examle lang is an instance of Language class.

Window progress indicator:

There is also a progress indicator on the MainWindowView header that could be used to show user current window status. To handle this status you can resolve IWindowProgressService service (or use WindowProgressService property of ViewModelBase) and use AddWaiter() method to add waiter to the service and RemoveWaiter() when the job is done. You can also handle WindowProgressChangedEvent by yourself using IEventAggregator.

Logging:

To log your app's work you can resolve ILoggingService that is just an adapter of Log4netLoggingService or use LoggingService property of ViewModelBase.

You can find the log file in you running assembly directory called cadlog.log.

Custom controls on window header:

You can also create some Your custom control on the window header. There are two regions used for this - Regions.HEADER_RIGHT_REGION and Regions.HEADER_LEFT_REGION. The left one is on the left side of header progress bar and the right one is on the right side. The height of the header is 22 pixels. Here is an example of how to register there Your views:

internal class HeaderModule : IModule
{
    public void OnInitialized(IContainerProvider containerProvider)
    {
        var region = containerProvider.Resolve<IRegionManager>();

        region.RegisterViewWithRegion(Regions.HEADER_RIGHT_REGION, typeof(RightControlView));
        region.RegisterViewWithRegion(Regions.HEADER_LEFT_REGION, typeof(LeftControlView));
    }

    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterForNavigation<RightControlView>();
        containerRegistry.RegisterForNavigation<LeftControlView>();
    }
}

Observables and Observers:

Abdrakov.Solutions provides You some methods to observe property changes in a bindable class. An example:

this.WhenPropertyChanged(x => x.BindableBrush).Subscribe((b) =>
{
    Debug.WriteLine($"Current brush is: {b}");
});

Where BindableBrush is declared as:

[Notify]
public SolidColorBrush BindableBrush { get; set; }

You should use DynamicData or ReactiveUI that provide more powerful work with Observer pattern.

Powered by: