Search code examples
c#wpfdispatcherresourcedictionaryitemcontainergenerator

How to change the loaded theme resource dictionary before or after an ItemsControl is generating its containers, but not in that time?


I dynamically load app resource dictionaries (2 of 3 loaded at a time):

  1. a base resource dictionary, always
  2. a Light.xaml theme file
  3. a Dark.xaml theme file

If I normally change the MergedDictionaries property's value when the main window is already Loaded, I get an exception (call stack here):

System.InvalidOperationException: 'Cannot call StartAt when content generation is in progress.'

If I change the MergedDictionaries property's value using Dispatcher.BeginInvoke, when I use a resource in code-behind from (1), it says through an exception that it is not loaded yet (like using a resource through StaticResource that does not exist).

I do not want to use an App.xaml file because, for having a single-instance application, I use a class that inherits from Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase and calls the code in my App.cs file.

I may call the LoadTheme method in a few places in the application code and I want to make it stable.

App.cs

(no XAML)

public class App : System.Windows.Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        LoadTheme(AppTheme.Light);
        var w = new MainWindow();
        ShutdownMode = ShutdownMode.OnMainWindowClose;
        MainWindow = w;
        w.Show();
    }
    internal ResourceDictionary MLight = null,
        MDark = null,
        MMain = null;
    internal ResourceDictionary GetLightThemeDictionary()
    {
        if (MLight == null)
        {
            MLight = new ResourceDictionary() { Source = new Uri("Themes/Light.xaml", UriKind.Relative) };
        }
        return MLight;
    }
    internal ResourceDictionary GetDarkThemeDictionary()
    {
        if (MDark == null)
        {
            MDark = new ResourceDictionary() { Source = new Uri("Themes/Dark.xaml", UriKind.Relative) };
        }
        return MDark;
    }
    internal ResourceDictionary GetMainDictionary()
    {
        if (MMain == null)
        {
            MMain = new ResourceDictionary() { Source = new Uri("AppResources.xaml", UriKind.Relative) };
        }
        return MMain;
    }
    internal void LoadTheme(AppTheme t)
    {
        //Dispatcher.BeginInvoke(new Action(() =>
        //{
            if (Resources.MergedDictionaries.Count == 2)
            {
                switch (t)
                {
                    case AppTheme.Dark:
                        Resources.MergedDictionaries[1] = GetDarkThemeDictionary();
                        break;
                    default:
                        Resources.MergedDictionaries[1] = GetLightThemeDictionary();
                        break;
                }
            }
            else if (Resources.MergedDictionaries.Count == 1)
            {
                switch (t)
                {
                    case AppTheme.Dark:
                        Resources.MergedDictionaries.Add(GetDarkThemeDictionary());
                        break;
                    default:
                        Resources.MergedDictionaries.Add(GetLightThemeDictionary());
                        break;
                }
            }
            else
            {
                Resources.MergedDictionaries.Clear();
                Resources.MergedDictionaries.Add(GetMainDictionary());
                LoadTheme(t);
            }
        //}), System.Windows.Threading.DispatcherPriority.Normal); // how to process this after the ItemsControl has generated its elements?
    }
}

I tried to make a test example but I failed - I created a program that works, because I set the ItemsControl.ItemsSource every time the template is applied. In my actual project, I set ItemsSource through data binding and sometimes manually, but I am not sure this is what misses from my actual project.

I use .NET Framework 4.7.2, VS 2019, Win 10 Pro.

Thank you.


Solution

  • The OnStartup method now looks like this (I just loaded the base dictionary and one of the themes before constructing the MainWindow):

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
    
        Resources.MergedDictionaries.Clear();
        Resources.MergedDictionaries.Add(GetMainDictionary());
        Resources.MergedDictionaries.Add(GetLightThemeDictionary());
    
        LoadTheme(AppTheme.Light);
    
        var w = new MainWindow();
    
        ShutdownMode = ShutdownMode.OnMainWindowClose;
        MainWindow = w;
    
        w.Show();
    }
    

    And I uncommented the 2 comments (that make use of the Dispatcher):

    internal void LoadTheme(AppTheme t)
    {
        Dispatcher.BeginInvoke(new Action(() =>
        {
            [...]
        }), System.Windows.Threading.DispatcherPriority.Normal);
    }