DynamicResource for Style BasedOn
Asked Answered
C

4

15

Building an application that has a custom 'High Contrast' theme for outdoor use that can be toggled on and off during runtime. This works fine by merging and un-merging a resource dictionary that contains styles like below...

<Style x:Key="{x:Type MenuItem}" TargetType="{x:Type MenuItem}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="Template" Value="{StaticResource Theme_MenuItemTemplate}"/>
</Style>

This works great when the usage of a menuitem doesn't specify a style. This isn't realistic though for many situations since there is no way to bind ItemsSource generated children without Styles. For example:

<ContextMenu.ItemContainerStyle>
    <Style TargetType="MenuItem">
        <Setter Property="Header" Value="{Binding Path=Name}"/>
        <Setter Property="IsCheckable" Value="True"/>
        <Setter Property="IsChecked" Value="{Binding Path=Checked}"/>
        <EventSetter Event="Checked" Handler="HistoryItem_Checked"/>
    </Style>
</ContextMenu.ItemContainerStyle>

Every other post on StackOverflow says you just need to do this...

<Style TargetType="MenuItem" BasedOn="{StaticResource {x:Type MenuItem}}">
    <!-- Your overrides -->
</Style>

But this doesn't work for my situation because my BasedOn can and will change at runtime (and of course you can't use DynamicResource extension on the BasedOn property). Doing this in my application currently leads to controls that override getting stuck with their style when the control was loaded while every other control correctly switches without reloading.

So my question...

Is there a way to get DynamicResource extension working for BasedOn or is there another method/hack I can implement to get this to work?

Cyprus answered 28/2, 2012 at 21:48 Comment(0)
C
8

Finally figured out a solution for a DynamicResouce for Style.BasedOn using an AttachedDependencyProperty.

Here is the fix for ItemsControl.ItemContainerStyle (can be easily modified to change FrameworkElement.Style)

public class DynamicContainerStyle
{
    public static Style GetBaseStyle(DependencyObject obj)
    {
        return (Style)obj.GetValue(BaseStyleProperty);
    }

    public static void SetBaseStyle(DependencyObject obj, Style value)
    {
        obj.SetValue(BaseStyleProperty, value);
    }

    // Using a DependencyProperty as the backing store for BaseStyle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty BaseStyleProperty =
        DependencyProperty.RegisterAttached("BaseStyle", typeof(Style), typeof(DynamicContainerStyle), new UIPropertyMetadata(DynamicContainerStyle.StylesChanged));

    public static Style GetDerivedStyle(DependencyObject obj)
    {
        return (Style)obj.GetValue(DerivedStyleProperty);
    }

    public static void SetDerivedStyle(DependencyObject obj, Style value)
    {
        obj.SetValue(DerivedStyleProperty, value);
    }

    // Using a DependencyProperty as the backing store for DerivedStyle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DerivedStyleProperty =
        DependencyProperty.RegisterAttached("DerivedStyle", typeof(Style), typeof(DynamicContainerStyle), new UIPropertyMetadata(DynamicContainerStyle.StylesChanged));

    private static void StylesChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (!typeof(System.Windows.Controls.ItemsControl).IsAssignableFrom(target.GetType()))
            throw new InvalidCastException("Target must be ItemsControl");

        var Element = (System.Windows.Controls.ItemsControl)target;

        var Styles = new List<Style>();

        var BaseStyle = GetBaseStyle(target);

        if (BaseStyle != null)
            Styles.Add(BaseStyle);

        var DerivedStyle = GetDerivedStyle(target);

        if (DerivedStyle != null)
            Styles.Add(DerivedStyle);

        Element.ItemContainerStyle = MergeStyles(Styles);
    }

    private static Style MergeStyles(ICollection<Style> Styles)
    {
        var NewStyle = new Style();

        foreach (var Style in Styles)
        {
            foreach (var Setter in Style.Setters)
                NewStyle.Setters.Add(Setter);

            foreach (var Trigger in Style.Triggers)
                NewStyle.Triggers.Add(Trigger);
        }

        return NewStyle;
    }
}

And here is an example...

<!-- xmlns:ap points to the namespace where DynamicContainerStyle class lives -->
<MenuItem Header="Recent" 
    ItemsSource="{Binding Path=RecentFiles}"
    IsEnabled="{Binding RelativeSource={RelativeSource Self}, Path=HasItems}"
    ap:DynamicContainerStyle.BaseStyle="{DynamicResource {x:Type MenuItem}}">
    <ap:DynamicContainerStyle.DerivedStyle>
        <Style TargetType="MenuItem">
            <EventSetter Event="Click"  Handler="RecentFile_Clicked"/>
        </Style>
    </ap:DynamicContainerStyle.DerivedStyle>
    <MenuItem.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding}"/>
        </DataTemplate>
    </MenuItem.ItemTemplate>
</MenuItem>

Here is a modified version that sets the FrameworkElement.Style instead in my answer to another post: Setting a local implicit style different from theme-style / alternative to BasedOn DynamicResource

Cyprus answered 29/2, 2012 at 20:33 Comment(1)
There is an easier way to 'copy' the base styles. I added this in a new answer.Mockingbird
M
4

I have a slight improvement for NtscCobalts answer:

    #region Type-specific function (FrameworkElement)
    private static void StylesChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        var mergedStyles = GetMergedStyles<FrameworkElement>(target, GetBaseStyle(target), GetDerivedStyle(target)); // NOTE: change type on copy

        var element = (FrameworkElement)target; // NOTE: change type on copy

        element.Style = mergedStyles;
    }
    #endregion Type-specific function (FrameworkElement)


    #region Reused-function 
    public static Style GetMergedStyles<T>(DependencyObject target, Style baseStyle, Style derivedStyle) where T : DependencyObject
    {
        if (!(target is T)) throw new InvalidCastException("Target must be " + typeof(T));

        if (derivedStyle == null) return baseStyle;
        if (baseStyle == null) return derivedStyle;

        var newStyle = new Style { BasedOn = baseStyle, TargetType = derivedStyle.TargetType };
        foreach (var setter in derivedStyle.Setters) newStyle.Setters.Add(setter);
        foreach (var trigger in derivedStyle.Triggers) newStyle.Triggers.Add(trigger);
        return newStyle;

    }
    #endregion Reused-function

You see, it is possible to simply set the base style, when creating a new style. This way the base styles from the base style are automatically in the hierarchy.

Mockingbird answered 15/11, 2013 at 13:17 Comment(0)
M
2

I have gotten wiser as time has passed and I would like to offer another option to handle different appearances, if the change is mostly colors (with of borders ext. should work too).

Create the colors you want as resources, and when you make the styles for your controls, use the colors as DynamicResources, NOT StaticResource, and this is the whole trick.

When you want to change the 'theme', simply load/override the color resources. Because of the dynamic bindings, the styles will automatically update/use the nearest definition of the resource.

Similarly, if there is an area where you want things to look different, simply set/override the color resources by setting new values in the Resources of a parent control.

I hope this is useable for others who come along =0)

Mockingbird answered 19/6, 2020 at 7:16 Comment(0)
H
1

Your Styles should be in a UIElement.Resources tag. This can be dynamically cleared and repopulated.

I do something similar but not as complex like this:

MobileApp.Get().Resources.MergedDictionaries.Clear();

Uri uri = new Uri("/Resources/DayModeButton.xaml", UriKind.Relative);
ResourceDictionary resDict = Application.LoadComponent(uri) as ResourceDictionary;

resDict["SelectedColor"] = selectedColor; //change an attribute of resdict

MobileApp.Get().Resources.MergedDictionaries.Add(resDict);
Hooray answered 28/2, 2012 at 22:27 Comment(2)
That is pretty much what I do for merging and unmerging resource dictionaries. I'm not sure what you mean by UIElement.Resoruces tag.Cyprus
I've tested placing the style in the UIElement.Resources instead of explicitly setting the style in <Whatever.Style> but it has the same effect and I find using <Whatever.Style> easier to read.Cyprus

© 2022 - 2024 — McMap. All rights reserved.