简体   繁体   中英

Managing WPF Theming while hosted in WinForms

I am in the process of replacing portions of an old WinForms application with WPF, with hopes of eventually going towards the MVVM paradigm with it.

The one major concern that was brought to me with this effort is to maintain the dynamic theming being used by the WinForms components through out the application. These are mostly a collection of old 2007 and 2010 Office themes.

My plan was to build a singleton object that launched the WPF application, add a resource dictionary with DynamicResource hook ups for colors for various controls, and then dynamically swap out another resource dictionary that actually contains the color definitions as the hosting WinForms application changes its theme.

This works perfectly as long as WPF is being hosted within a WPF window. If the WPF is being hosted within a WinForms container, the resource dictionary definitely gets swapped out, but the view does not get refreshed. I know this because once i mouse-over a button on the view, its color THEN gets updated.

I recently ripped out the code into an independent solution to try testing it, so I'll add them here. This example code was an independent test to change the theme once in a simple WinForms project:

UserControlResourceDictionary.xaml

<SolidColorBrush x:Key="WhiteBrush" Color="White" />

<!--Region Containers-->

<Style TargetType="{x:Type UserControl}">
    <Setter Property="Background" Value="{DynamicResource DefaultBackgroundBrush}"/>
</Style>

<Style TargetType="{x:Type Panel}">
    <Setter Property="Background" Value="{DynamicResource DefaultBackgroundBrush}"/>
</Style>

<Style TargetType="{x:Type Grid}" BasedOn="{StaticResource {x:Type Panel}}"/>
<Style TargetType="{x:Type StackPanel}" BasedOn="{StaticResource {x:Type Panel}}"/>

<!--End Region Containers-->

<!--Region TextBox-->

<Style TargetType="{x:Type TextBox}">
    <Setter Property="FontSize" Value="11" />
    <Setter Property="FontWeight" Value="Normal" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBox}">
                <Border BorderThickness="1"
                        BorderBrush="{DynamicResource TextBoxBorderBrush}"
                        Background="{DynamicResource TextBoxBackgroundBrush}"
                        x:Name="Border">
                    <ScrollViewer x:Name="PART_ContentHost" />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Setter TargetName="Border" Property="Background"
                                Value="{DynamicResource TextBoxMouseOverBrush}" />
                    </Trigger>
                    <Trigger Property="IsKeyboardFocusWithin" Value="True">
                        <Setter TargetName="Border" Property="Background" Value="{DynamicResource TextBoxKeyboardFocusBrush}" />
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!--End Region TextBox-->

<!--Region Button-->

<Style TargetType="{x:Type Button}">
    <Setter Property="SnapsToDevicePixels" Value="True" />
    <Setter Property="OverridesDefaultStyle" Value="True" />
    <Setter Property="Foreground" Value="{DynamicResource FontColorBrush}" />
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Width" Value="90" />
    <Setter Property="Height" Value="25" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border CornerRadius="{DynamicResource ButtonCornerRadius}"
                        BorderThickness="1"
                        BorderBrush="{DynamicResource DefaultButtonBorderBrush}"
                        Background="{DynamicResource DefaultButtonBrush}"
                        x:Name="Border">
                    <ContentPresenter Margin="2"
                                      HorizontalAlignment="Center"
                                      VerticalAlignment="Center"
                                      RecognizesAccessKey="True" />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Setter TargetName="Border" Property="Background"
                                Value="{DynamicResource DefaultMouseOverBrush}" />
                    </Trigger>
                    <Trigger Property="IsPressed" Value="True">
                        <Setter TargetName="Border" Property="Background" Value="{DynamicResource ButtonPressBrush}" />
                        <Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource ButtonPressBorderBrush}" />
                    </Trigger>
                    <Trigger Property="IsDefaulted" Value="True">
                        <Setter TargetName="Border" Property="BorderBrush" Value="Black" />
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="Border" Property="Background" Value="{DynamicResource DefaultDisabledBrush}" />
                        <Setter TargetName="Border" Property="BorderBrush"
                                Value="{DynamicResource DisabledBorderBrush}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!--End Region Button-->

Office2007BlackStyle.xaml

<!--Region Colors-->

<Color x:Key="ButtonLight">
    #EDEEF0
</Color>

<Color x:Key="ButtonDark">
    #BBC0C6
</Color>

<Color x:Key="ButtonDisableLight">
    #F3F6F8
</Color>

<Color x:Key="ButtonDisableDark">
    #CBD5DF
</Color>

<Color x:Key="ButtonPressLight">
    #F4BC81
</Color>

<Color x:Key="ButtonPressDark">
    #EB7A05
</Color>

<Color x:Key="ButtonMouseOverLight">
    #FBEDBD
</Color>

<Color x:Key="ButtonMouseOverDark">
    #F4B100
</Color>

<Color x:Key="DefaultButtonBorderColor">
    #898785
</Color>

<Color x:Key="TextBoxBorderColor">
    #ABC1DE
</Color>

<Color x:Key="DisabledBorderColor">
    #A1BDCF
</Color>

<Color x:Key="ButtonPressBorderColor">
    #9B8259
</Color>

<Color x:Key="FontColor">
    #464646
</Color>

<Color x:Key="BackgroundColor">
    #535353
</Color>

<Color x:Key="GroupBoxColor">
    #1E1E1E
</Color>

<!--End Region Colors-->

<CornerRadius x:Key="ButtonCornerRadius">
    2
</CornerRadius>


<!--Region Brushes-->

<SolidColorBrush x:Key="DefaultButtonBorderBrush" Color="{DynamicResource DefaultButtonBorderColor}" />
<SolidColorBrush x:Key="TextBoxBorderBrush" Color="{DynamicResource TextBoxBorderColor}"/>
<SolidColorBrush x:Key="DisabledBorderBrush" Color="{DynamicResource DisabledBorderColor}" />
<SolidColorBrush x:Key="DefaultLabelBrush" Color="{DynamicResource ButtonLight}" />
<SolidColorBrush x:Key="ButtonPressBorderBrush" Color="{DynamicResource ButtonPressBorderColor}"/>
<SolidColorBrush x:Key="FontColorBrush" Color="{DynamicResource FontColor}"/>
<SolidColorBrush x:Key="DefaultBackgroundBrush" Color="{DynamicResource BackgroundColor}"/>
<SolidColorBrush x:Key="GroupBoxColorBrush" Color="{DynamicResource GroupBoxColor}"/>
<SolidColorBrush x:Key="TextBoxBackgroundBrush" Color="White"/>
<SolidColorBrush x:Key="TextBoxMouseOverBrush" Color="White"/>
<SolidColorBrush x:Key="TextBoxKeyboardFocusBrush" Color="White"/>

<LinearGradientBrush x:Key="DefaultButtonBrush" StartPoint="0,0" EndPoint="0,1.0">
    <GradientStop Color="{DynamicResource ButtonLight}" Offset="0" />
    <GradientStop Color="{DynamicResource ButtonDark}" Offset=".5" />
    <GradientStop Color="{DynamicResource ButtonLight}" Offset="1" />
</LinearGradientBrush>

<LinearGradientBrush x:Key="DefaultDisabledBrush" StartPoint="0,0" EndPoint="0,1.0">
    <GradientStop Color="{DynamicResource ButtonDisableLight}" Offset="0" />
    <GradientStop Color="{DynamicResource ButtonDisableDark}" Offset=".5" />
    <GradientStop Color="{DynamicResource ButtonDisableLight}" Offset="1" />
</LinearGradientBrush>

<LinearGradientBrush x:Key="ButtonPressBrush" StartPoint="0,0" EndPoint="0,1.0">
    <GradientStop Color="{DynamicResource ButtonPressLight}" Offset="0" />
    <GradientStop Color="{DynamicResource ButtonPressDark}" Offset=".5" />
    <GradientStop Color="{DynamicResource ButtonPressLight}" Offset="1" />
</LinearGradientBrush>

<LinearGradientBrush x:Key="DefaultMouseOverBrush" StartPoint="0,0" EndPoint="0,1.0">
    <GradientStop Color="{DynamicResource ButtonMouseOverLight}" Offset="0" />
    <GradientStop Color="{DynamicResource ButtonMouseOverDark}" Offset=".5" />
    <GradientStop Color="{DynamicResource ButtonMouseOverLight}" Offset="1" />
</LinearGradientBrush>

<!--End Region Brushes-->

AppHost.cs

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Threading;

namespace EmbeddedWPFTest
{
    public static class AppHost
    {
        private static readonly object AppLock = Guid.NewGuid();
        private static Application _application;
        private static ResourceDictionary _currentTheme;
        private static ResourceDictionary _controlDictionary;
        private static ResourceDictionary _resourceDictionary;
        private static Dictionary<string, ResourceDictionary> _themes;

        public static Dispatcher Dispatcher { get; set; }

        public static Application CurrentApplication
        {
            get
            {
                lock (AppLock)
                {
                    if (_application == null)
                    {
                        _application = new Application();

                        LoadDictionaries();
                        InitializeApplication();

                   }
                }

                return _application;
            }
        }

        private static void InitializeApplication()
        {
            Application.Current.Resources.MergedDictionaries.Add(_resourceDictionary);
            Application.Current.Resources.MergedDictionaries.Add(_controlDictionary);
            _currentTheme = Application.LoadComponent(
        new Uri(@"/EmbeddedWPFTest;component/Resources/Office2007BlueStyle.xaml",
            UriKind.Relative))
    as ResourceDictionary;
            Application.Current.Resources.MergedDictionaries.Add(_currentTheme);
        }

        public static void ChangeTheme()
        {
            Application.Current.Resources.MergedDictionaries.Remove(_currentTheme);
            InitializeApplication();


                    _currentTheme = Application.LoadComponent(
                            new Uri(@"/EmbeddedWPFTest;component/Resources/Office2007BlackStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary;
                    Application.Current.Resources.MergedDictionaries.Add(_currentTheme);


        }

        private static void LoadDictionaries()
        {
            _resourceDictionary =
                Application.LoadComponent(new Uri(@"/EmbeddedWPFTest;component/Resources/ResourceDictionary.xaml",
                    UriKind.Relative)) as ResourceDictionary;
            _controlDictionary =
                Application.LoadComponent(new Uri(@"/EmbeddedWPFTest;component/Resources/UserControlResourceDictionary.xaml",
                    UriKind.Relative)) as ResourceDictionary;
            _themes = new Dictionary<string, ResourceDictionary>
            {
                {
                    "Office2007BlueStyle",
                    Application.LoadComponent(
                            new Uri(@"/EmbeddedWPFTest;component/Resources/Office2007BlueStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary
                },
                {
                    "Office2007BlackStyle",
                    Application.LoadComponent(
                            new Uri(@"/EmbeddedWPFTest;component/Resources/Office2007BlackStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary
                }
            };
        }
    }
}

My goal was, when the hosting WinForms application changed its theme, I could wire up to the event, and have AppHost change out for the corresponding resource dictionary. I left out that portion of the code for simplicity. In the test that I'm running right now, it happens as a result of a button press in the view. Again, if the view is hosted in a WPF container, it works fine, but hosted within a WinForms container, it does not refresh or repaint.

I tried every solution that I could find on the Internet. The only way that I could get this to work was to create a new copy of the view. This isn't the most elegant solution, but I figured that as long as you preserve the same instance of the underlying view model for each view, it's not a terrible solution.

Now, I'm using MVVMLight in my effort to update this code base. MVVMLight comes with a nifty messenger utility that's handy for asynchronous communication between modules. I decided to utilize that in communicating between my AppHost class, and the WinForms hosts.

New AppHost.cs

public static class AppHost
    {
        private static readonly object AppLock = Guid.NewGuid();
        private static Application _application;
        private static ResourceDictionary _currentTheme;
        private static ResourceDictionary _controlDictionary;
        private static ResourceDictionary _resourceDictionary;
        private static Dictionary<string, ResourceDictionary> _themes;
        private static KryptonManager _kryptonManager;
        private static IMessenger _messengerInstance;

        /// <summary>
        /// Gets or sets an instance of a <see cref="IMessenger" /> used to
        /// broadcast messages to other objects. If null, this class will
        /// attempt to broadcast using the Messenger's default instance.
        /// </summary>
        private static IMessenger MessengerInstance
        {
            get
            {
                return _messengerInstance ?? Messenger.Default;
            }
            set
            {
                _messengerInstance = value;
            }
        }

        public static Application CurrentApplication
        {
            get
            {
                lock (AppLock)
                {
                    if (_application == null)
                    {
                        _application = new Application();

                        LoadDictionaries();
                        InitializeApplication();
                        ChangeTheme(PaletteModeManager.Custom);

                        KryptonManager.GlobalPaletteChanged += KryptonManagerGlobalPaletteChanged;
                        _kryptonManager = new KryptonManager();
                    }
                }

                return _application;
            }
        }

        private static void KryptonManagerGlobalPaletteChanged(object sender, EventArgs e)
        {
            ChangeTheme(_kryptonManager.GlobalPaletteMode);
        }

        private static void InitializeApplication()
        {
            Application.Current.Resources.MergedDictionaries.Add(_resourceDictionary);
            Application.Current.Resources.MergedDictionaries.Add(_controlDictionary);
        }

        public static void ChangeTheme(PaletteModeManager manager)
        {
            Application.Current.Resources.MergedDictionaries.Remove(_currentTheme);

            switch (manager)
            {
                case PaletteModeManager.Office2007Blue:
                    _currentTheme = _themes["Office2007BlueStyle"];
                    Application.Current.Resources.MergedDictionaries.Add(_currentTheme);
                    break;
                case PaletteModeManager.Office2007Black:
                    _currentTheme = _themes["Office2007BlackStyle"];
                    Application.Current.Resources.MergedDictionaries.Add(_currentTheme);
                    break;
                default:
                    _currentTheme = _themes["Office2007BlueStyle"];
                    Application.Current.Resources.MergedDictionaries.Add(_currentTheme);
                    break;
            }

            MessengerInstance.Send(new ThemeChangedMessage());
        }

        private static void LoadDictionaries()
        {
            _resourceDictionary =
                Application.LoadComponent(new Uri(@"/ApplicationHost;component/Resources/ResourceDictionary.xaml",
                    UriKind.Relative)) as ResourceDictionary;
            _controlDictionary =
                Application.LoadComponent(new Uri(@"/ApplicationHost;component/Resources/UserControlResourceDictionary.xaml",
                    UriKind.Relative)) as ResourceDictionary;
            _themes = new Dictionary<string, ResourceDictionary>
            {
                {
                    "Office2007BlueStyle",
                    Application.LoadComponent(
                            new Uri(@"/ApplicationHost;component/Resources/Office2007BlueStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary
                },
                {
                    "Office2007BlackStyle",
                    Application.LoadComponent(
                            new Uri(@"/ApplicationHost;component/Resources/Office2007BlackStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary
                }
            };
        }
    }

So here, when AppHost receives notification from the WinForms host (Krypton in this instance), it swaps out for the correct color resource dictionary, and then sends out an asynchronous message using MVVMLight's messaging functionality to notify listeners that the theme has changed.

BaseFormsWrapper.cs

public class BaseFormsWrapper : UserControl
{
    public Panel PanelBasePanel;
    private ElementHost _wpfHost;
    private IMessenger _messengerInstance;

    public BaseFormsWrapper()
    {
        InitializeComponent();
        MessengerInstance.Register<ThemeChangedMessage>(this, HandleThemeChanged);
    }

    private void HandleThemeChanged(ThemeChangedMessage obj)
    {
        var instance = Activator.CreateInstance(_wpfHost.Child.GetType());
        var oldView = _wpfHost.Child;
        _wpfHost.Child = (UIElement) instance;

        var view = oldView as ViewBase;
        var newView = instance as ViewBase;

        if ((view != null) && (newView != null))
        {
            newView.DataContext = view.DataContext;
        }
    }

    /// <summary>
    /// Gets or sets an instance of a <see cref="IMessenger" /> used to
    /// broadcast messages to other objects. If null, this class will
    /// attempt to broadcast using the Messenger's default instance.
    /// </summary>
    private IMessenger MessengerInstance
    {
        get
        {
            return _messengerInstance ?? Messenger.Default;
        }
        set
        {
            _messengerInstance = value;
        }
    }

    public UIElement HostedControl
    {
        get { return _wpfHost.Child; }
        set { _wpfHost.Child = value; }
    }

    private void InitializeComponent()
    {
        PanelBasePanel = new Panel();
        _wpfHost = new ElementHost();
        PanelBasePanel.SuspendLayout();
        SuspendLayout();
        // 
        // panelBasePanel
        // 
        PanelBasePanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
        PanelBasePanel.Controls.Add(_wpfHost);
        PanelBasePanel.Dock = DockStyle.Fill;
        PanelBasePanel.Location = new Point(0, 0);
        PanelBasePanel.Margin = new Padding(0);
        PanelBasePanel.Name = "PanelBasePanel";
        PanelBasePanel.Size = new Size(1126, 388);
        PanelBasePanel.TabIndex = 0;
        // 
        // wpfHost
        // 
        _wpfHost.BackColor = SystemColors.ControlLightLight;
        _wpfHost.BackgroundImageLayout = ImageLayout.None;
        _wpfHost.Dock = DockStyle.Fill;
        _wpfHost.Location = new Point(0, 0);
        _wpfHost.Name = "_wpfHost";
        _wpfHost.Size = new Size(1126, 388);
        _wpfHost.TabIndex = 0;
        _wpfHost.Child = null;
        // 
        // BaseFormsWrapper
        // 
        AutoScaleDimensions = new SizeF(6F, 13F);
        AutoSize = true;
        Controls.Add(PanelBasePanel);
        Name = "BaseFormsWrapper";
        Size = new Size(1126, 388);
        PanelBasePanel.ResumeLayout(false);
        ResumeLayout(false);
    }
}

Before letting the GC handle the old view instance, I preserve its DataContext (view model) by stuffing it into the new view instance.

I'm still working on my proof-of-concept work item to make sure all of this works when put through the paces, but seems to be snappy and functional in my initial testing.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM