Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic resource merging not applying styles to first control

I tried to apply new style for my custom controls by merging the style resources to the application resources while adding the control to UI, but new styles are not applied to the controls for the first time.

Sample control

CustomTextBoxExt.cs

 public class CustomTextBoxExt : TextBox
    {
        static CustomTextBoxExt()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomTextBoxExt), new FrameworkPropertyMetadata(typeof(CustomTextBoxExt)));
        }
    }

default style Generic.xaml

 <Style x:Key="TextBoxExtStyle" TargetType="{x:Type local:CustomTextBoxExt}">
    <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" />
    <Setter Property="BorderBrush" Value="Red" />
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
    <Setter Property="HorizontalContentAlignment" Value="Left" />
    <Setter Property="FocusVisualStyle" Value="{x:Null}" />
    <Setter Property="AllowDrop" Value="True" />
    <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst" />
    <Setter Property="Stylus.IsFlicksEnabled" Value="False" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:CustomTextBoxExt}">
                <Border
                    x:Name="border"
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    SnapsToDevicePixels="True">
                    <ScrollViewer
                        x:Name="PART_ContentHost"
                        Focusable="False"
                        HorizontalScrollBarVisibility="Hidden"
                        VerticalScrollBarVisibility="Hidden" />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="border" Property="Opacity" Value="0.56" />
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="border" Property="BorderBrush" Value="#FF7EB4EA" />
                    </Trigger>
                    <Trigger Property="IsKeyboardFocused" Value="True">
                        <Setter TargetName="border" Property="BorderBrush" Value="#FF569DE5" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>

        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="IsInactiveSelectionHighlightEnabled" Value="True" />
                <Condition Property="IsSelectionActive" Value="False" />
            </MultiTrigger.Conditions>
            <Setter Property="SelectionBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}" />
        </MultiTrigger>
    </Style.Triggers>
</Style>
<Style BasedOn="{StaticResource TextBoxExtStyle}" TargetType="{x:Type local:CustomTextBoxExt}" />

Custom theme TextBoxExtStyle.xaml

<Style x:Key="MaterialTextBoxExtStyle" TargetType="{x:Type local:CustomTextBoxExt}">
    <Setter Property="FocusVisualStyle" Value="{x:Null}" />
    <Setter Property="Foreground" Value="#DD000000" />
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="BorderBrush" Value="#FF9E9E9E" />
    <Setter Property="BorderThickness" Value="0,0,0,1" />
    <Setter Property="FontSize" Value="12" />
    <Setter Property="FontWeight" Value="Normal" />
    <Setter Property="SelectionBrush" Value="#FF0279FF" />
    <Setter Property="AllowDrop" Value="true" />
    <Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
    <Setter Property="HorizontalContentAlignment" Value="Left" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
    <Setter Property="AllowDrop" Value="true" />
    <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst" />
    <Setter Property="Stylus.IsFlicksEnabled" Value="False" />
    <Setter Property="CaretBrush" Value="#DD000000" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:CustomTextBoxExt}">
                <Grid>
                    <Border
                        x:Name="border"
                        Padding="{TemplateBinding Padding}"
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        SnapsToDevicePixels="true">
                        <ScrollViewer
                            x:Name="PART_ContentHost"
                            Background="Transparent"
                            Focusable="False"
                            HorizontalScrollBarVisibility="Hidden"
                            VerticalScrollBarVisibility="Hidden" />
                    </Border>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Setter TargetName="border" Property="BorderBrush" Value="#FF757575" />
                        <Setter TargetName="border" Property="Background" Value="Transparent" />
                        <Setter Property="Foreground" Value="#DD000000" />
                        <Setter Property="CaretBrush" Value="#DD000000" />
                    </Trigger>
                    <Trigger Property="IsKeyboardFocused" Value="true">
                        <Setter TargetName="border" Property="BorderBrush" Value="#FF0279FF" />
                        <Setter Property="BorderThickness" Value="0,0,0,2" />
                        <Setter Property="Padding" Value="0,0,0,-1" />
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="border" Property="Background" Value="Transparent" />
                        <Setter TargetName="border" Property="BorderBrush" Value="#FFE0E0E0" />
                        <Setter Property="Foreground" Value="#60000000" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style BasedOn="{StaticResource MaterialTextBoxExtStyle}" TargetType="{x:Type local:CustomTextBoxExt}" />

and using attached property, trying to change style from default style,

public class SkinExt
{
    public static string GetTheme(DependencyObject obj)
    {
        return (string)obj.GetValue(ThemeProperty);
    }

    public static void SetTheme(DependencyObject obj, string value)
    {
        obj.SetValue(ThemeProperty, value);
    }

    // Using a DependencyProperty as the backing store for Theme.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ThemeProperty =
        DependencyProperty.RegisterAttached("Theme", typeof(string), typeof(SkinExt), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.Inherits, new PropertyChangedCallback(OnVisualStyleChanged)));


    private static void OnVisualStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue != e.OldValue && !string.IsNullOrEmpty(e.NewValue.ToString()) /*&& d is FrameworkElement && (d as FrameworkElement).IsLoaded*/)
        {
            SkinExt.ApplyTheme(d, e.NewValue.ToString());
        }
    }

    internal static void ApplyTheme(DependencyObject obj, string style)
    {
        Type itemType = obj.GetType();

        List<string> styles = GetDictionaries(obj.GetType().Name.ToString(), style);

        if (styles != null && styles.Count > 0)
        {
            foreach (var path in styles)
            {
                var rdict = new ResourceDictionary() { Source = new Uri(path, UriKind.RelativeOrAbsolute) };
                bool alreadyExist = false;
                foreach (var dictionaryFiles in Application.Current.Resources.MergedDictionaries)
                {
                    if (dictionaryFiles.Source.OriginalString.Contains(path))
                    {
                        alreadyExist = true;
                        break;
                    }
                }

                if (!alreadyExist)
                {
                    Application.Current.Resources.MergedDictionaries.Add(rdict);
                    Console.WriteLine(path);
                }
            }

        }
    }

    internal static List<string> GetDictionaries(String type, string style)
    {
        List<string> styles = new List<string>();
        #region Switch

        switch (type)
        {
            case "CustomTextBoxExt":
                styles.Add("/TextBoxExt;component/TextBoxExt/TextBoxExtStyle.xaml");
                break;
            case "ButtonExt":
                styles.Add("/TextBoxExt;component/ButtonExt/ButtonExtStyle.xaml");
                break;
            case "Label":
                styles.Add("/TextBoxExt;component/LabelStyle.xaml");
                break;
        }

        # endregion

        return styles;
    }

}

setting

local:SkinExt.Theme="Material" 

in mainwindow/grid works as expected when children are added directly. But, when using below lazyextension style is not working.

public static class LazyLoadExtensions
{
    public static LazyUIElementCollection GetLazyChildrens(DependencyObject obj)
    {
        return (LazyUIElementCollection)obj.GetValue(LazyChildrensProperty);
    }

    public static void SetLazyChildrens(DependencyObject obj, LazyUIElementCollection value)
    {
        obj.SetValue(LazyChildrensProperty, value);
    }

    public static readonly DependencyProperty LazyChildrensProperty =
        DependencyProperty.RegisterAttached("LazyChildrens", typeof(LazyUIElementCollection), typeof(LazyLoadExtensions), new PropertyMetadata(OnLazyChildrensChanged));

    private static void OnLazyChildrensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var wrapPanel = d as WrapPanel;
        var childrens = LazyLoadExtensions.GetLazyChildrens(wrapPanel);
        for (int i = 0; i < childrens.Count; i++)
        {
            var child = childrens[i];

                wrapPanel.Children.Add(child);
        }

    }
}

public class LazyUIElementCollection : List<UIElement>
{
    public LazyUIElementCollection()
    {

    }
}

Works

 <Grid local:SkinExt.Theme="Material">
    <WrapPanel x:Name="wrapPanel">
        <!--<local:LazyLoadExtensions.LazyChildrens>-->
        <!--<local:LazyUIElementCollection>-->
        <StackPanel Margin="10">
            <TextBlock Margin="0,0,0,8" Text="MS Label" />
            <Label
                Width="200"
                Height="25"
                Content="Material" />
        </StackPanel>
        <StackPanel Margin="10">
            <TextBlock Margin="0,0,0,8" Text="Custom TextBox" />
            <local:CustomTextBoxExt
                Width="200"
                Height="25"
                Text="Material" />
        </StackPanel>
        <!--</local:LazyUIElementCollection>-->
        <!--</local:LazyLoadExtensions.LazyChildrens>-->
    </WrapPanel>
</Grid>

Not Working

<Grid local:SkinExt.Theme="Material">
    <WrapPanel x:Name="wrapPanel">
        <local:LazyLoadExtensions.LazyChildrens>
            <local:LazyUIElementCollection>
                <StackPanel Margin="10">
                    <TextBlock Margin="0,0,0,8" Text="MS Label" />
                    <Label
                        Width="200"
                        Height="25"
                        Content="Material" />
                </StackPanel>
                <StackPanel Margin="10">
                    <TextBlock Margin="0,0,0,8" Text="Custom TextBox" />
                    <local:CustomTextBoxExt
                        Width="200"
                        Height="25"
                        Text="Material" />
                </StackPanel>
            </local:LazyUIElementCollection>
        </local:LazyLoadExtensions.LazyChildrens>
    </WrapPanel>
</Grid>

Works for second item

Style applying correctly for second customtextboxext

<Grid local:SkinExt.Theme="Material">
    <WrapPanel x:Name="wrapPanel">
        <local:LazyLoadExtensions.LazyChildrens>
            <local:LazyUIElementCollection>
                <StackPanel Margin="10">
                    <TextBlock Margin="0,0,0,8" Text="MS Label" />
                    <Label
                        Width="200"
                        Height="25"
                        Content="Material" />
                </StackPanel>
                <StackPanel Margin="10">
                    <TextBlock Margin="0,0,0,8" Text="Custom TextBox" />
                    <local:CustomTextBoxExt
                        Width="200"
                        Height="25"
                        Text="Material" />
                    <local:CustomTextBoxExt
                        Width="200"
                        Height="25"
                        Text="Material" />
                </StackPanel>
            </local:LazyUIElementCollection>
        </local:LazyLoadExtensions.LazyChildrens>
    </WrapPanel>
</Grid>

Reproducible sample : https://drive.google.com/open?id=1iB9sY90T7aRaaRTzVc1EvE2qFU13fHG7

Check the above sample and let me know your ideas

like image 546
Durga S Avatar asked Oct 16 '22 03:10

Durga S


2 Answers

cool project !

I'm not exactly sure why the problem occurs but it seems like a timing problem.

I found the following workaround: In LazyLoadExtensions:

private static void OnLazyChildrensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var wrapPanel = d as WrapPanel;
        var childrens = LazyLoadExtensions.GetLazyChildrens(wrapPanel);
        for (int i = 0; i < childrens.Count; i++)
        {
            var child = childrens[i];

            //The dictionaries containing the styles are added
            SkinExt.SetTheme(child, SkinExt.GetTheme(wrapPanel));
            wrapPanel.Children.Add(child);
        }

    }

This way the dictionaries containing the style are merged before the the children are added and the styles are correctly applied.

Edit: Since you can't change the LazyLoadExtensions class:

In SkinExt class:

internal static void ApplyTheme(DependencyObject obj, string style)
    {
        Type itemType = obj.GetType();

        List<string> styles = GetDictionaries(obj.GetType().Name.ToString(), style);

        if (styles != null && styles.Count > 0)
        {
            foreach (var path in styles)
            {
                var rdict = new ResourceDictionary() { Source = new Uri(path, UriKind.RelativeOrAbsolute) };
                bool alreadyExist = false;
                foreach (var dictionaryFiles in Application.Current.Resources.MergedDictionaries)
                {
                    if (dictionaryFiles.Source.OriginalString.Contains(path))
                    {
                        alreadyExist = true;
                        break;
                    }
                }

                if (!alreadyExist)
                {
                    Application.Current.Resources.MergedDictionaries.Add(rdict);
                    if (obj is FrameworkElement frameworkElement && frameworkElement.IsInitialized)
                    {
                        //The style won't be applied automaticaly
                        frameworkElement.Style = rdict.Values.OfType<Style>().First(s => s.TargetType == itemType);
                    }
                    else
                    {
                        //Nothing to do, style will be applied automaticaly
                    }
                    Console.WriteLine(path);
                }
            }

        }
    }

If the IsInitialized property is true, the style from the added ressourceDictionnary won't be applied, you can check it in debugger.

https://learn.microsoft.com/en-gb/dotnet/api/system.windows.frameworkelement.isinitialized?view=netcore-3.1#System_Windows_FrameworkElement_IsInitialized

like image 72
Ostas Avatar answered Oct 30 '22 21:10

Ostas


If it's helpful, applying theme inside dispatcher works as expected ,

  d.Dispatcher.BeginInvoke(new Action(() =>
            {
                SkinExt.ApplyTheme(d, e.NewValue.ToString());
            }));
like image 27
WPFUser Avatar answered Oct 30 '22 20:10

WPFUser