【问题标题】:Changing value of a ThemeResource at runtime does not reflect in other views在运行时更改 ThemeResource 的值不会反映在其他视图中
【发布时间】:2017-04-16 13:19:46
【问题描述】:

我在我的 UWP 应用中使用自定义主题词典。我在运行时更改了 ThemeResource 的值。此更改仅反映在主视图中,而不反映在其他视图中。即使我在更改资源的值后创建了一个新视图,新视图也只使用资源的初始值。有什么我做错了吗?

这就是我改变资源价值的方式。

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

我的辅助视图的 XAML:

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

即使我的主视图也有相同的 XAML。

这是完整的项目。 Download Repo as zip

【问题讨论】:

  • 我使用的是样式而不是主题!通过使用样式,我们可以在不重启应用的情况下更新外观。
  • @JayZuo-MSFT 即使更改了资源的值,辅助视图仍然具有最初在资源字典中指定的值。
  • 如果我使用多个视图导致封送异常,则无法使用绑定
  • @Romasz 其次,它是否可以更新在一个视图中使用资源的 UI 元素?为什么不能在其他打开的视图中更新?你看过我的样品吗?

标签: c# xaml uwp win-universal-app uwp-xaml


【解决方案1】:

我认为这是设计使然。当我们为一个应用创建多个窗口时,每个窗口的行为都是独立的。每个窗口都在自己的线程中运行。每个窗口中使用的Application.Resources也是独立的。

Application.Resources 是一个ResourceDictionary 对象,而ResourceDictionary 类继承自DependencyObject,所以Application.Resources 也是一个DependencyObject 实例。

所有DependencyObject 实例必须在与应用的当前Window 关联的UI 线程上创建。这是由系统强制执行的,这有两个重要含义为您的代码:

  • 使用来自两个DependencyObject 实例的API 的代码将始终在同一线程上运行,该线程始终是UI 线程。在这种情况下,您通常不会遇到线程问题。
  • 不在主 UI 线程上运行的代码无法直接访问 DependencyObject,因为 DependencyObject 仅与 UI 线程具有线程关联性。只有在 UI 线程上运行的代码才能更改甚至读取依赖属性的值。例如,您使用 .NET Task 或显式 ThreadPool 线程启动的工作线程将无法读取依赖项属性或调用其他 API。

有关详细信息,请参阅RemarksDependencyObject 下的DependencyObject 和线程

所以每个Window 都有自己的Application.Resources。在辅助视图中,Application.Resources 是从您的 ResourceDictionary 重新评估的。 BackgroundBrush 不受主视图设置的影响,如下代码所示

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

您只是更改与主视图的Window 关联的Application.Current.Resources 实例。

如果您希望辅助视图使用与主视图相同的画笔,我认为您可以将此颜色存储在主视图中,然后在创建辅助视图时应用它。例如,我在App 类中添加了一个名为BackgroundBrushColor 的静态字段,然后像下面这样使用它:

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);
}

我已更改代码以修改每个视图中的资源。如果在更改值之前在 SecondaryPage.xaml.cs 的 ThemeChanged 方法中设置断点,您可以看到资源的值已更改为更新后的值。但它没有反映在视图中。

这里的问题是因为您的ThemeChanged 事件是在主视图中触发的,所以它在主视图的线程中运行,因此您在SecondaryPage.xaml.cs 中使用了ThemeManager_ThemeChanged 方法也将在主视图的线程中运行。这导致ThemeManager_ThemeChanged 方法中的Application.Current.Resources 仍然获得与主视图关联的ResourceDictionary 实例。这就是为什么资源的值已经更改为更新的值并且不会反映在视图中的原因。要清楚地看到这一点,您可以在调试时利用Threads Window

要解决这个问题,您可以使用this.Dispatcher.RunAsync 方法在正确的线程中运行您的代码,如下所示:

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

但是,您仍然会收到类似这样的错误:“应用程序调用了一个为不同线程编组的接口。”这是因为SolidColorBrush类也继承自DependencyObject。为了解决这个问题,我建议将HighAccentColorBrush 类型从SolidColorBrush 更改为Color,然后像这样使用它:

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

【讨论】:

  • 所以如果我有 100 个主题资源,你的意思是说我的 App.xaml.cs 中应该有 100 个静态属性。现在不是主题资源概念本身正在失效吗?其次,Application.Resources 的含义字面意思是应用程序的资源,对吗?所以它应该是所有观点的共同点?或者每个视图都被视为不同的应用程序。我在这里看到了一些设计级别的缺陷。如果它以您提到的方式工作。您可以与您的团队核实并恢复吗?默认邮件应用程序也处理这种情况吗?你能告诉我他们是如何处理这件事的吗?
  • 每个窗口中的 application.current.resources 也可能是独立的。您可以与团队中的某个人核对并用二进制回答这个问题吗??
  • 我已更改代码以修改每个视图中的资源。如果在更改值之前在 SecondaryPage.xaml.cs 的 ThemeChanged 方法中设置断点,您可以看到资源的值已更改为更新后的值。但它没有反映在视图中。 PS:检查我的最新代码。 github.com/Kumara-Krishnan/ThemePOC
  • @MojoJojo 我已经更新了我的答案来解释这一点,请检查一下。对于默认邮件应用程序,我不确定它如何处理这种情况,但它在辅助视图中只使用了一种颜色,所以我想我的解决方案应该可以工作。我不确定为什么需要更改 100 个主题资源,正如您在默认邮件应用程序中看到的那样,它只更改一种颜色。
【解决方案2】:

以这种方式使用主题。

在您的主题页面上单击按钮添加此逻辑

    void OnClicked(object sender, System.EventArgs e)
    {
        var btn = sender as Button;

        MessagingCenter.Send(this, "ThemeButtonClicked", btn.BackgroundColor);

        Application.Current.Properties["Theme"] = btn.BackgroundColor;
        Application.Current.SavePropertiesAsync();

        Navigation.PopAsync(true);

    }

在所有其他页面订阅上述通知,如下所示。

 MessagingCenter.Subscribe<Theme, Color>(this, "ThemeButtonClicked", OnThemeChanged);

在订阅者通知时添加以下方法以执行。

  private void OnThemeChanged(Theme source, Color cl)
    {
        this.BackgroundColor = cl;
        layout.BackgroundColor = cl;

    }

这样堆栈上所有打开的实例也会更新主题。希望这能回答您的问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多