How to use ResourceDictionary with Dependency Injection Options pattern?

162 Views Asked by At

I have a .NET Framework 4.8 WPF (MVVM) project I am working on. I have to use .NET Framework, I have opted to use the latest version of the Framework that I can. However, I am also using some libraries typically associated with .NET applications, namely, Microsoft.Extensions.Hosting and the related Dependency Injection library.

I put a project exemplifying the issue I have here.

This project is quite simplified from my real code, but it does get the point across, which is this: how do I reference a ResourceDictionary Style in a UserControl, where the value set to the Style is supplied at run-time?

From this project, here are some key lines:

  • Line 13: ProjectB\ResourceDictionary.xaml
  • Line 44: ProjectC\AnotherControlUserControl.xaml
  • Line 28: ProjectA\App.xaml.cs
  • Line 32: ProjectA\App.xaml.cs
  • Line 7: ProjectA\appsettings.json

Basically, I have a hex color code in appsettings.json. I then add this to my IHost configuration. I then map that to a class ColorSettings using the Options pattern. My actual Window is composed of a nested UserControl, which in turn has another nested UserControl, and these eventually surface on my Window through either the Host.CreateDefaultBuilder().ConfigureServices(...) method OR by directly instantiating certain user controls (which do not have a corresponding ViewModel) in the XAML (e.g. line 27 from ProjectC\MyDisplayUserControl.xaml).

The problem I have is that when I bind the Rectangle in AnotherControlUserControl (see line 44) I want it to get the appropriate color hex code from appsettings.json, but it does not, instead just rendering it white.

I have been at this problem for many hours now, between yesterday and today. So I can't provide an exhaustive list of everything I have tried, but here are some things I recall attempting:

  • Created another class ColorSettingsFactory that has a public property of type ColorSettings, of which I set the value in the DI code, e.g. ColorSetting = serviceProvider.GetRequiredService<IOptions<ColorSetting>>().Value (XAML can't instantiate a class without a parameterless constructor), then added that into my ResourceDictionary.xaml instead of ColorSettings (line 6 of ResourceDictionary.xaml)
  • Played around with the binding syntax in various ways and formats... could not even begin to enumerate all the combinations I tried. I did try DynamicResource as well as StaticResource. I tried various ways of configuring with Mode=TwoWay and UpdateSourceTrigger=PropertyChanged, etc.
  • I originally had not made ColorSettings to implement INotifyPropertyChanged (here I am through Community Toolkit MVVM NuGet ObservableObject) but I added that and that didn't do anything either
  • I tried forcing the UI to update with startupWindow.UpdateLayout() in App.xaml.cs
  • Something that DID work, but which I do NOT prefer, is I injected IOptions<ColorSettings> into my MyDisplayViewModel class, then added Fill="{Binding ColorSettings.ColorHeader}" to AnotherControlUserControl XAML file (line 44, again). I do not prefer this solution because of the inelegance. I would like to keep all Style related definitions in ResourceDictionary.xaml if possible, partially to keep these sort of things centralized to one place. Additionally, I could see it being useful in other scenarios where I want to store run-time values for other objects (in this case my Rectangle is used simply for a background color on a Grid "cell", but there are many other possible objects that could be made configurable, and I don't want to inject a zillion IOptions<...> into a given ViewModel... and I could go on, basically I just don't prefer this workaround)
  • I looked at various Stack Overflow posts, including but not limited to this, this, and this.

Ultimately, when my class ColorSettings is updated in the DI code, I would expect that the value in ResourceDictionary.xaml is updated, and that is then reflected wherever referenced. It seems to me that there is some disconnect between the DI Container and the XAML, but I do not know how to remedy it. While I have the aforementioned workaround, if there is a way to update ResourceDictionary.xaml based on my configuration file appsettings.json, which is then reflected wherever utilized, I would be greatly interested.

Thank you!

1

There are 1 best solutions below

0
mm8 On

In ProjectB/ResourceDictionary.xaml, you are creating a ColorSettings object that you never set the ColorHeader property of:

<local:ColorSettings x:Key="Colors"/>

If you want to want to use the value in your appsettings.json file, you need to grab that value and set the property of the actual resource that you are binding to somewhere.

You should also ensure that there is only a single ColorSettings object/resource in your app. Remove the resource from the resource dictionary and define it as some kind of singleton:

public class ColorSettings : ObservableObject
{
    public static ColorSettings Instance { get; } = new ColorSettings();
    
    private string _colorHeader;

    public string ColorHeader { get => _colorHeader; set => SetProperty(ref _colorHeader, value); }
}

Change the binding in the resource dictionary to bind to the singleton:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:ProjectB;assembly=ProjectB">

    <Style x:Key="RectangleHeaderFillStatic" TargetType="Rectangle">
        <Setter Property="Fill" Value="#F8CBAD"/>
    </Style>

    <Style x:Key="RectangleHeaderFillDynamic" TargetType="Rectangle">
        <Setter Property="Fill" Value="{Binding Source={x:Static local:ColorSettings.Instance}, Path=ColorHeader}"/>
    </Style>

</ResourceDictionary>

Then set the property of the data-bound singleton:

private void Application_Startup(object sender, StartupEventArgs e)
{
    MainWindow startupWindow = _appHost.Services.GetRequiredService<MainWindow>();
    var colorSettings = _appHost.Services.GetService<IOptions<ColorSettings>>();
    ColorSettings.Instance.ColorHeader = colorSettings?.Value?.ColorHeader;
    startupWindow.Show();
}

To summarize; adding a (meant-to-be) application-wide resource to a resource dictionary that you merge into more than one view is not a good idea. You should define the resource once in a central place.