Forum Xamarin.Forms

Is there a way to combine DynamicResource and IValueConverter

I have some styles that change at runtime, so naturally I am using XAML like this:

<Style x:Key="ThemedFrame" TargetType="Frame">
    <Setter Property="BackgroundColor" Value="{DynamicResource ThemeColorSurfacedp1}" />
    <Setter Property="BorderColor" Value="{DynamicResource ThemeColorSurfacedp24}" />
</Style>

This approach, however, requires I create a variety of resources for all possible combinations of Color elevations (and other things such as opacity).

I'd prefer to do something like this:

<Style x:Key="ThemedFrame" TargetType="Frame">
    <Setter Property="BackgroundColor" Value="{DynamicResource ThemeColorSurface, Elevation=1}" />
    <Setter Property="BorderColor" Value="{DynamicResource ThemeColorSurface, Elevation=24" />
</Style>

I've been trying to work out if it is possible to create a custom markup extension that either works the same way as DynamicResource with additional properties (as per my example), or somehow combine a DynamicResource with a Converter.

Does anyone know if this is even remotely possible?

Best Answer

  • mjfreelancingmjfreelancing ✭✭
    Accepted Answer

    I ended up solving this by Binding to a static class (called ThemeColors, inheriting BindableObject) and creating dependency properties for each of the properties on my model's interface. My view model (or anything really) updates the state of this static class and everything flows through. The XAML syntax then looks like this:

    <Style x:Key="IndicatorViewPanelStyle" TargetType="pancake:PancakeView">
      <Setter Property="BackgroundColor" Value="{Binding ThemeColors.Surface,
         Converter={StaticResource SurfaceElevationConverter},
         ConverterParameter={x:Static models:ElevationLevel.dp8}}" />
    </Style>
    

    So, with the aid of a converter and some constant values I can now set a color and apply an elevation (or an opacity using a different converter).

Answers

  • LandLuLandLu Member, Xamarin Team Xamurai

    Firstly, try to define two themes file. One could be like:

    <ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="App.DarkTheme">
        <Style x:Key="ThemedFrame" TargetType="Frame">
            <Setter Property="BackgroundColor" Value="#FF000000" />
            <Setter Property="BorderColor" Value="#FF000000" />
        </Style>
    </ResourceDictionary>
    public partial class DarkTheme : ResourceDictionary
    {
        public DarkTheme()
        {
            InitializeComponent();
        }
    }
    

    Another theme:

    <ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="App.WhiteTheme">
        <Style x:Key="ThemedFrame" TargetType="Frame">
            <Setter Property="BackgroundColor" Value="#FFFFFFFF" />
            <Setter Property="BorderColor" Value="#FFFFFFFF" />
        </Style>
    </ResourceDictionary>
    public partial class WhiteTheme : ResourceDictionary
    {
        public WhiteTheme()
        {
            InitializeComponent();
        }
    }
    

    Then load one of the default theme in your App:

    <Application.Resources>
        <ResourceDictionary Source="WhiteTheme.xaml"/>
    </Application.Resources>
    

    At last, we could complete the theme's changing in the view model even without an converter:

    if (Elevation == 1)
    {
        App.Current.Resources = new DarkTheme();
    }
    else if (Elevation == 24)
    {
        App.Current.Resources = new WhiteTheme();
    }
    //...
    

    Do not forget to use DynamicResource to bind the style:

    <Frame Style="{DynamicResource ThemedFrame}">
        <!-- controls -->
    </Frame>
    
  • mjfreelancingmjfreelancing Member ✭✭

    My situation is more complicated than that. I already have two theme files, one for light and the other for dark. The issue is that they are both driven by user-defined colors, where even the surface colors can be blended with a primary color. On top of this there are various elevations (only application to dark mode).

    I already have code that updates the resource dictionary with the required color palette but I'm trying to avoid creating custom styles for every combination, such as 38% opactity at elevation 24.

    I want to be able to say using this color, and this opactity, and this elevation, use this color (consumed by the DynamicResource in the style).

    As an example, I'm doing the equivalent of this at the moment:

    SetCurrentTheme(new DarkThemeStyles());

    Where DarkThemeStyles is a XAML file.

    This call also updates the App resource dictionary with the source colors (user driven via a model)

    private void AppendThemeColors(IDictionary<string, object> resources)
    {
      var properties = typeof(IThemeColors).GetTypeInfo().DeclaredProperties.Where(prop => prop.CanRead);
    
      foreach (var property in properties)
      {
        var key = $"{ThemeKeyPrefix}{property.Name}";
        var value = property.GetValue(ViewModel.ThemeColors);
    
        resources[key] = value;
      }
    }
    

    As well as this horrible code that builds keys for each of the elevations:

    private void AppendThemeSurfaceColors(IDictionary<string, object> resources)
    {
      var surfaceElevation = new SurfaceElevation(ViewModel.ThemeColors);
      var elevations = EnumHelper.GetValueList<ElevationLevel>();
    
      foreach (var elevation in elevations)
      {
        surfaceElevation.Elevation = elevation;
        var surfaceColor = surfaceElevation.SurfaceColor;
    
        resources[$"{ThemeKeyPrefix}Surface{elevation}"] = surfaceColor;
      }
    }
    

    The 'ThemeKeyPrefix' is 'ThemeColor', so when you see this code:

    <Style x:Key="ThemedFrame" TargetType="Frame">
      <Setter Property="BorderColor" Value="{DynamicResource ThemeColorSurfacedp24}" />
    </Style>
    

    You can see it will update the UI with the required color and elevation. The deficiency I am facing, however, is that I have no way to apply an opacity - which I want to perform from the View as it has nothing to do with the view model, and I don't want to have to create all possible combinations and keep it maintained.

    It would result in dozens of keys that will never get used, something like:

    ThemeColorSurfacedp24Opacity38
    ThemeColorSurfacedp24Opacity60
    ThemeColorSurfacedp24Opacity87

    etc.

    Something like this would be much neater and less maintenance:

    <Style x:Key="ThemedFrame" TargetType="Frame">
      <Setter Property="BorderColor" Value="{DynamicResource ThemeColorSurface}, Elevation=24, Opacity=38" />
    </Style>
    

    (I'm using AARRGGBB color to achieve opacity)

    Hope this makes sense and is a little clearer.

  • mjfreelancingmjfreelancing Member ✭✭
    Accepted Answer

    I ended up solving this by Binding to a static class (called ThemeColors, inheriting BindableObject) and creating dependency properties for each of the properties on my model's interface. My view model (or anything really) updates the state of this static class and everything flows through. The XAML syntax then looks like this:

    <Style x:Key="IndicatorViewPanelStyle" TargetType="pancake:PancakeView">
      <Setter Property="BackgroundColor" Value="{Binding ThemeColors.Surface,
         Converter={StaticResource SurfaceElevationConverter},
         ConverterParameter={x:Static models:ElevationLevel.dp8}}" />
    </Style>
    

    So, with the aid of a converter and some constant values I can now set a color and apply an elevation (or an opacity using a different converter).

Sign In or Register to comment.