Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DynamicResource for Style BasedOn

Tags:

styles

wpf

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?

like image 968
NtscCobalt Avatar asked Feb 28 '12 21:02

NtscCobalt


2 Answers

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

like image 134
NtscCobalt Avatar answered Nov 18 '22 22:11

NtscCobalt


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)

like image 24
aliceraunsbaek Avatar answered Nov 18 '22 22:11

aliceraunsbaek