Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing value of a ThemeResource at runtime does not reflect in other views

I am using custom themedictionary in my UWP app. I change the value of a ThemeResource at runtime. This change is reflected only in the mainview and not the other views. Even if i create a new view after changing the resource's value the new view uses only the intial value of the resource. Is there anything I'm doing wrong?

This is how I change my resource's value.

(Application.Current.Resources["BackgroundBrush"] as SolidColorBrush).Color = Windows.UI.Colors.Black;

My Secondary View's XAML:

<Grid Background="{ThemeResource BackgroundBrush}"/>

Even my main view has the same XAML.

Here's the complete project. Download Repo as zip

like image 358
Razor Avatar asked Apr 16 '17 13:04

Razor


1 Answers

I'd think this is by design. When we create multiple windows for an app, each window behaves independently. Each window operates in its own thread. The Application.Resources used in each window is also independent.

Application.Resources is a ResourceDictionary object and ResourceDictionary class inherits from DependencyObject, so Application.Resources is also a DependencyObject instance.

All DependencyObject instances must be created on the UI thread that is associated with the current Window for an app. This is enforced by the system, and there are two important implications of this for your code:

  • Code that uses API from two DependencyObject instances will always be run on the same thread, which is always the UI thread. You don't typically run into threading issues in this scenario.
  • Code that is not running on the main UI thread cannot access a DependencyObject directly because a DependencyObject has thread affinity to the UI thread only. Only code that runs on the UI thread can change or even read the value of a dependency property. For example a worker thread that you've initiated with a .NET Task or an explicit ThreadPool thread won't be able to read dependency properties or call other APIs.

For more info, please see DependencyObject and threading under Remarks of DependencyObject.

So each Window has its own Application.Resources. In secondary view, the Application.Resources is re-evaluated from your ResourceDictionary. The BackgroundBrush would not be affected by the setting in main view as with the following code

(Application.Current.Resources["BackgroundBrush"] as SolidColorBrush).Color = Windows.UI.Colors.Black;

you are only changing the Application.Current.Resources instance associated with main view's Window.

If you want the secondary view to use the same brush as the main view, I think you can store this color in main view and then apply it when creating secondary view. For example, I add a static field named BackgroundBrushColor in App class and then use it like the following:

private void ThemeChanger_Click(object sender, RoutedEventArgs e)
{
    App.BackgroundBrushColor = Windows.UI.Color.FromArgb(Convert.ToByte(random.Next(255)), Convert.ToByte(random.Next(255)), Convert.ToByte(random.Next(255)), Convert.ToByte(random.Next(255)));

    (Application.Current.Resources["BackgroundBrush"] as SolidColorBrush).Color = App.BackgroundBrushColor;
}

private async Task CreateNewViewAsync()
{
    CoreApplicationView newView = CoreApplication.CreateNewView();
    int newViewId = 0;

    await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        (Application.Current.Resources["BackgroundBrush"] as SolidColorBrush).Color = App.BackgroundBrushColor;

        Frame frame = new Frame();
        frame.Navigate(typeof(SecondaryPage), null);
        Window.Current.Content = frame;
        // You have to activate the window in order to show it later.
        Window.Current.Activate();

        newViewId = ApplicationView.GetForCurrentView().Id;
    });
    bool viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
}

I have changed the code to modify the resource in each view. if you set a breakpoint in ThemeChanged method in SecondaryPage.xaml.cs before changing the value you can see that the resource's value has already changed to the updated one. But its not reflecting in the view.

The problem here is because your ThemeChanged event is triggered in main view, so it's running in main view's thread, therefore the ThemeManager_ThemeChanged method you've used in SecondaryPage.xaml.cs will also running in main view's thread. This leads to the Application.Current.Resources in ThemeManager_ThemeChanged method still get the ResourceDictionary instance associated with main view. That's why the resource's value has already changed to the updated one and won't reflect in the view. To see this clearly, you can take advantage of Threads Window while debugging.

To solve this issue, you can use this.Dispatcher.RunAsync method to run your code in right thread like the following:

private async void ThemeManager_ThemeChanged(Utils.ThemeManager theme)
{
    await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        ((SolidColorBrush)Application.Current.Resources["BackgroundBrush"]).Color = theme.HighAccentColorBrush.Color;
    });
}

However, with this you will still get an error like: "The application called an interface that was marshalled for a different thread." This is because SolidColorBrush class also inherits from DependencyObject. To solve this issue, I'd suggest changing HighAccentColorBrush type from SolidColorBrush to Color and then use it like:

private async void ThemeManager_ThemeChanged(Utils.ThemeManager theme)
{
    await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        ((SolidColorBrush)Application.Current.Resources["BackgroundBrush"]).Color = theme.HighAccentColorBrush;
    });
}
like image 189
Jay Zuo Avatar answered Nov 06 '22 22:11

Jay Zuo