【问题标题】:Windows Store App UI updateWindows 应用商店应用 UI 更新
【发布时间】:2012-03-29 18:42:02
【问题描述】:

我正在为 Windows 8 编写一个 Windows Store App 玩具应用程序。 它只有一个带有TextBlock 的xaml 页面。该页面的 MyTimer 类为 DataContext

this.DataContext = new MyTimer();

MyTimer 实现INotifyPropertyChanged 并且属性Time 的更新是通过计时器进行的:

public MyTimer(){
    TimerElapsedHandler f = new TimerElapsedHandler(NotifyTimeChanged);
    TimeSpan period = new TimeSpan(0, 0, 1);
    ThreadPoolTimer.CreatePeriodicTimer(f, period);
}

private void NotifyTimeChanged(){
    if (this.PropertyChanged != null){
        this.PropertyChanged(this, new PropertyChangedEventArgs("Time"));
    }
}

TextBlock 在时间上有一个数据绑定

<TextBlock Text="{Binding Time}" />

当我运行应用程序时,出现以下异常:

System.Runtime.InteropServices.COMException was unhandled by user code

有消息

The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))

真正的问题是我正在更新类 MyTimer 的属性,而不是 GUI 本身, 我想不通,但我认为解决方案应该使用 this one 之类的东西。

【问题讨论】:

    标签: c# xaml windows-runtime windows-store-apps


    【解决方案1】:

    是的,您是从线程池线程而不是 UI 线程通知属性更改。您需要在计时器回调中将通知编组回 UI 线程。现在,您的视图模型与您的视图分离(一件好事),因此它没有直接链接到Dispatcher 基础设施。因此,您要做的就是将正确的SynchronizationContext 交给它进行通信。为此,您需要在构造过程中捕获当前的SynchronizationContext,或者允许将其显式传递给一个有利于测试的构造函数,或者如果您从 UI 线程开始初始化对象。

    整个shebang看起来像这样:

    public class MyTimer
    {
        private SynchronizationContext synchronizationContext;
    
        public MyTimer() : this(SynchronizationContext.Current)
        {
        }
    
        public MyTimer(SynchronizationContext synchronizationContext)
        {
            if(this.synchronizationContext == null)
            {
                throw new ArgumentNullException("No synchronization context was specified and no default synchronization context was found.")
            }
    
            TimerElapsedHandler f = new TimerElapsedHandler(NotifyTimeChanged);
            TimeSpan period = new TimeSpan(0, 0, 1);
            ThreadPoolTimer.CreatePeriodicTimer(f, period);
        }
    
        private void NotifyTimeChanged()
        {
            if(this.PropertyChanged != null)
            {
                this.synchronizationContext.Post(() =>
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("Time"));
                    });
            }
        }
    }
    

    【讨论】:

    • 非常感谢。这表明我每次使用异步调用时都需要使用 SynchronizationContext……我也会考虑使用 async 关键字做类似的事情。
    • 是的,如果你使用 C# 4.5 的 await 关键字,你会默认得到这个。
    • 感谢您的回答!我只想添加一个旁注:如果您想在 UI 上下文中回调,请确保在 UI 构建之前捕获SynchronizationContext(我使用OnLaunched 事件处理程序来执行此操作),否则您获取的上下文将不起作用。延伸阅读:codeproject.com/Articles/31971/…
    【解决方案2】:

    一种方法是在循环中等待Task.Delay(),而不是使用计时器:

    class MyTimer : INotifyPropertyChanged
    {
        public MyTimer()
        {
            Start();
        }
    
        private async void Start()
        {
            while (true)
            {
                await Task.Delay(TimeSpan.FromSeconds(1));
                PropertyChanged(this, new PropertyChangedEventArgs("Time"));
            }
        }
    
        public event PropertyChangedEventHandler PropertyChanged = delegate { };
    
        public DateTime Time { get { return DateTime.Now; } }
    }
    

    如果您在 UI 线程上调用构造函数,它也会在那里调用 PropertyChanged。好消息是完全相同的代码也可以在 WPF 中运行(在 .Net 4.5 和 C# 5 下)。

    【讨论】:

    • 不是说这段代码行不通,而是 IME Task.Delay 的分辨率不是很高。在 WPF、Win8 和 WP8 测试中,我将 CancellationTokenSource.CancelAfter 设置为 8 分钟,而 UI 只到了 7:56 分钟。
    • @Stonetip 它肯定应该比 1 秒有更好的分辨率,我猜你的代码中还有其他事情发生。此外,Task.Delay() 不是 CacelAfter(),尽管我希望它们具有相同的分辨率。
    • Task.Delay 确实具有比 1 秒更好的分辨率,但您希望它以毫秒为单位,然后我在 WPF、Win8 和 Win Phone 8 上的测试显示不一致的行为高达每分钟 0.5 秒。但是,我发现 ThreadPoolTimer 类在后两种环境中运行良好。
    【解决方案3】:

    这个博客的代码怎么样:

    http://metrowindows8.blogspot.in/2011/10/metro-tiles.html

    这对我有用。 我必须将 ThreadPoolTimer 对象传递给我的委托函数

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-05-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-14
      相关资源
      最近更新 更多