[WPF] 跨线程控制窗体UI

摘要:
呼叫线程无法存取此对象在WPF、WinForm这些应用程序中,必需是UI线程才能控制窗体。namespaceWpfApplication1{publicpartialclassMainWindow:Window{//FieldsprivatereadonlySystem.Threading.Timer_timer=null;privateint_count=0;//ConstructorspublicMainWindow(){//BaseInitializeComponent();//Timer_timer=newSystem.Threading.Timer;}//HandlersprivatevoidTimer_Ticked{_count++;this.TextBlock001.Text=_count.ToString();}}}使用Dispatcher对象跨线程非UI线程如果要控制窗体,必须要将控制窗体的程序逻辑封装成为委派,再将这个委派提交给UI线程去执行,藉由这个流程非UI线程就能够跨线程控制窗体。以此类推能得知Silverlight、WindowsPhone等等应用程序平台,也会提供对应的解决方案来让开发人员完成跨线程工作。

呼叫线程无法存取此对象

在WPF、WinForm这些应用程序中,必需是UI线程才能控制窗体。如果像是下列的范例程序一样,使用了非UI线程来控制窗体,那就会看到内容为「呼叫线程无法存取此对象,因为此对象属于另外一个线程」的InvalidOperationException例外错误。

[WPF] 跨线程控制窗体UI第1张

<Window x: 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"   Width="525">
    <TextBlock x:Name="TextBlock001"  FontSize="72" />
</Window>
namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        // Fields
        private readonly System.Threading.Timer _timer = null;

        private int _count = 0;
               

        // Constructors
        public MainWindow()
        {
            // Base
            InitializeComponent();

            // Timer
            _timer = new System.Threading.Timer(this.Timer_Ticked, null, 0, 100);
        }


        // Handlers
        private void Timer_Ticked(Object stateInfo)
        {
            _count++;            
            this.TextBlock001.Text = _count.ToString();
        }
    }
}

使用Dispatcher对象跨线程

非UI线程如果要控制窗体,必须要将控制窗体的程序逻辑封装成为委派,再将这个委派提交给UI线程去执行,藉由这个流程非UI线程就能够跨线程控制窗体。而在WPF应用程序中,非UI线程可以透过WPF提供的Dispatcher对象来提交委派。

参考数据:

MSDN - 使用 Dispatcher 建置响应性更佳的应用程序
昏睡领域 - [Object-oriented] 线程

<Window x: 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"   Width="525">
    <TextBlock x:Name="TextBlock001"  FontSize="72" />
</Window>
namespace WpfApplication2
{
    public partial class MainWindow : Window
    {
        // Fields
        private readonly System.Threading.Timer _timer = null;

        private int _count = 0;


        // Constructors
        public MainWindow()
        {
            // Base
            InitializeComponent();

            // Timer
            _timer = new System.Threading.Timer(this.Timer_Ticked, null, 0, 100);
        }


        // Handlers
        private void Timer_Ticked(Object stateInfo)
        {
            _count++;
            Action methodDelegate = delegate()
            {
                this.TextBlock001.Text = _count.ToString();
            };
            this.Dispatcher.BeginInvoke(methodDelegate);            
        }
    }
}

使用SynchronizationContext类别跨线程

在WPF应用程序中可以透过WPF提供的Dispatcher对象来完成跨线程工作,而在WinForm应用程序中则是需要透过WinForm提供的Invoke方法、BeginInvoke方法来完成跨线程工作。以此类推能得知Silverlight、Windows Phone等等应用程序平台,也会提供对应的解决方案来让开发人员完成跨线程工作。

每个应用程序平台都提供各自的跨线程解决方案这件事,对于开发共享函式库、框架的开发人员来说,就代表了要花不少的精力才能让函式库、框架适用于各种应用程序平台。为了整合不同平台跨线程的解决方案,在.NET Framework中将这些解决方案抽象化为统一的SynchronizationContext类别,再由各个应用程序平台去提供对应的实作。自此之后开发共享函式库、共享框架的开发人员,只要透过SynchronizationContext类别,就能完成适用于不同平台的跨线程功能。

必须值得一提的是,SynchronizationContext类别设计出来之后,应用范围已经不单单适用于跨线程控制窗体,在设计软件架构线程模型之类的场合,也会发现它的身影,非常推荐有兴趣的开发人员找相关的资料学习。

参考数据:

MSDN - 不可或缺的 SynchronizationContext

<Window x: 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"   Width="525">
    <TextBlock x:Name="TextBlock001"  FontSize="72" />
</Window>
namespace WpfApplication3
{
    public partial class MainWindow : Window
    {
        // Fields
        private readonly System.Threading.SynchronizationContext _syncContext = null;

        private readonly System.Threading.Timer _timer = null;

        private int _count = 0;


        // Constructors
        public MainWindow()
        {
            // Base
            InitializeComponent();

            // SyncContext
            _syncContext = System.Threading.SynchronizationContext.Current;

            // Timer
            _timer = new System.Threading.Timer(this.Timer_Ticked, null, 0, 100);
        }


        // Handlers
        private void Timer_Ticked(Object stateInfo)
        {
            _count++;
            System.Threading.SendOrPostCallback methodDelegate = delegate(object state)
            {
                this.TextBlock001.Text = _count.ToString();
            };
            _syncContext.Post(methodDelegate, null);
        }
    }
}

跨线程Binding数据对象

在WPF应用程序中提供了Binding数据对象的功能,透过这个功能就能将数据对象的属性直接呈现在窗体上。而数据对象如果有实作INotifyPropertyChanged接口、INotifyCollectionChanged接口...等等更新通知接口,就可以透过事件的方式用来通知资料内容更新,例如说:INotifyPropertyChanged接口就是藉由PropertyChanged事件来通知数据内容更新。

Binding功能会去处理这些数据内容更新事件,并且在收到这些事件之后去取得数据内容来更新窗体。而也因为Binding功能会去更新窗体,所以引发这些通知事件的线程必须是UI线程,这样才能让整个Binding功能正常运作,不会产生「呼叫线程无法存取此对象,因为此对象属于另外一个线程」的InvalidOperationException例外错误。

<Window x: 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"   Width="525">
    <TextBlock x:Name="TextBlock001"  FontSize="72" Text="{Binding Path=Count}" />
</Window>
namespace WpfApplication4
{
    public partial class MainWindow : Window
    {
        // Fields
        private readonly System.Threading.SynchronizationContext _syncContext = null;

        private readonly DataObject _dataObject = null;


        // Constructors
        public MainWindow()
        {
            // Base
            InitializeComponent();

            // SyncContext
            _syncContext = System.Threading.SynchronizationContext.Current;

            // DataObject
            _dataObject = new DataObject();
            _dataObject.SetSynchronizationContext(_syncContext);

            // DataContext
            this.DataContext = _dataObject;
        }
    }
}
namespace WpfApplication4
{
    public class DataObject : INotifyPropertyChanged
    {
        // Fields     
        private readonly System.Threading.Timer _timer = null;

        private System.Threading.SynchronizationContext _syncContext = null;

        private int _count = 0;


        // Constructors
        public DataObject()
        {
            // Timer
            _timer = new System.Threading.Timer(this.Timer_Ticked, null, 0, 100);
        }
        

        // Properties
        public int Count
        {
            get { return _count; }
            set
            {
                _count = value;
                this.OnPropertyChanged("Count");
            }
        }


        // Methods        
        public void SetSynchronizationContext(System.Threading.SynchronizationContext syncContext)
        {
            // SyncContext
            _syncContext = syncContext;
        }


        // Handlers
        private void Timer_Ticked(Object stateInfo)
        {
            this.Count++;
        }


        // Events
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string name)
        {
            System.Threading.SendOrPostCallback methodDelegate = delegate(object state)
            {
                var handler = this.PropertyChanged;
                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs(name));
                }
            };
            _syncContext.Post(methodDelegate, null);            
        }
    }
}

前一个跨线程Binding数据对象范例中,做为数据对象的DataObject对象,设计上很理想的在数据对象内部透过SynchronizationContext类别完成跨线程的工作。而在真实的开发环境中,数据对象常常是由另外一个系统所提供、并且无法改写(也不应该改写,因为改写代表将显示功能污染进其他系统),这时可以套用装饰者模式(Decorator Pattern)的「精神」,来完成跨线程Binding数据对象的功能。

<Window x: 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"   Width="525">
    <TextBlock x:Name="TextBlock001"  FontSize="72" Text="{Binding Path=Count}" />
</Window>
namespace WpfApplication5
{
    public partial class MainWindow : Window
    {
        // Fields
        private readonly System.Threading.SynchronizationContext _syncContext = null;

        private readonly DataObject _dataObject = null;

        private readonly DataObjectDecorator _dataObjectDecorator = null;


        // Constructors
        public MainWindow()
        {
            // Base
            InitializeComponent();

            // SyncContext
            _syncContext = System.Threading.SynchronizationContext.Current;

            // DataObject
            _dataObject = new DataObject();

            // DataObjectDecorator
            _dataObjectDecorator = new DataObjectDecorator(_dataObject);
            _dataObjectDecorator.SetSynchronizationContext(_syncContext);

            // DataContext
            this.DataContext = _dataObjectDecorator;
        }
    }
}
namespace WpfApplication5
{
    public class DataObject : INotifyPropertyChanged
    {
        // Fields     
        private readonly System.Threading.Timer _timer = null;

        private int _count = 0;


        // Constructors
        public DataObject()
        {
            // Timer
            _timer = new System.Threading.Timer(this.Timer_Ticked, null, 0, 100);
        }


        // Properties
        public int Count
        {
            get { return _count; }
            set
            {
                _count = value;
                this.OnPropertyChanged("Count");
            }
        }


        // Handlers
        private void Timer_Ticked(Object stateInfo)
        {
            this.Count++;
        }


        // Events
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string name)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}
namespace WpfApplication5
{
    public class DataObjectDecorator : INotifyPropertyChanged
    {
        // Fields
        private readonly DataObject _dataObject = null;

        private System.Threading.SynchronizationContext _syncContext = null;


        // Constructors
        public DataObjectDecorator(DataObject dataObject)
        {
            // DataObject
            _dataObject = dataObject;
            _dataObject.PropertyChanged += this.DataObject_PropertyChanged;
        }


        // Properties
        public int Count
        {
            get { return _dataObject.Count; }
            set { _dataObject.Count = value; }
        }


        // Methods        
        public void SetSynchronizationContext(System.Threading.SynchronizationContext syncContext)
        {
            // SyncContext
            _syncContext = syncContext;
        }


        // Handlers
        private void DataObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            System.Threading.SendOrPostCallback methodDelegate = delegate(object state)
            {
                this.OnPropertyChanged(e.PropertyName);
            };
            _syncContext.Post(methodDelegate, null);
        }


        // Events
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string name)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

跨线程Binding数据对象(.NET 3.5之后、包含.NET3.5)

上列套用装饰者模式(Decorator Pattern)的「精神」,来完成跨线程Binding数据对象的功能,其实要加入的唯一功能,就是将INotifyPropertyChanged接口的PropertyChanged事件,由非UI线程转换为UI线程来通知数据内容更新。这样的设计方式在对象种类少、对象属性不多的情景是可行的,但当对象属性多的场合,例如说有50个对象属性,那套用装饰者模式就必须要装饰出50个对象属性,这听起来光是打字工作量就会让人崩溃,一整个是很不符合人性的设计。

最近经由老狗大大 (http://www.dotblogs.com.tw/sanctuary/)的提点,发现在.NET3.5之后、包含.NET3.5,在Binding数据对象的设计上,有了一些新的变更。其中一个变更就是在Binding数据对象的功能中,非UI线程所引发的资料内容更新事件,在背景会被转换为UI线程去执行。经由这样的特性,开发人员就不需要硬套装饰者模式来建立转换线程的数据对象,直接使用数据对象原生的线程就可以,这样能够减低程序对象的复杂度、并且大幅提升开发的效率。

但要特别说的是,Binding功能这个跨线程的特性,虽然经由下列的范例程序验证是能够正常运作的,但在网络上或是MSDN中没有看到相关的技术文件(或是我没找到@@)。开发人员在使用这个特性做为设计依据时,必须要小心斟酌的使用。

参考数据:

WPF, Data Binding & Multithreading

<Window x: 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"   Width="525">
    <TextBlock x:Name="TextBlock001"  FontSize="72" Text="{Binding Path=Count}" />
</Window>
namespace WpfApplication6
{
    public partial class MainWindow : Window
    {
        // Fields
        private readonly DataObject _dataObject = null;


        // Constructors
        public MainWindow()
        {
            // Base
            InitializeComponent();

            // DataObject
            _dataObject = new DataObject();

            // DataContext
            this.DataContext = _dataObject;
        }
    }
}
namespace WpfApplication6
{
    public class DataObject : INotifyPropertyChanged
    {
        // Fields     
        private readonly System.Threading.Timer _timer = null;

        private int _count = 0;


        // Constructors
        public DataObject()
        {
            // Timer
            _timer = new System.Threading.Timer(this.Timer_Ticked, null, 0, 100);
        }


        // Properties
        public int Count
        {
            get { return _count; }
            set
            {
                _count = value;
                this.OnPropertyChanged("Count");
            }
        }


        // Handlers
        private void Timer_Ticked(Object stateInfo)
        {
            this.Count++;
        }


        // Events
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string name)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

原始码下载

原始码下载:ThreadBindingDemo.rar

免责声明:文章转载自《[WPF] 跨线程控制窗体UI》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇关于cas-client单点登录客户端拦截请求和忽略/排除不需要拦截的请求URL的问题(不需要修改任何代码,只需要一个配置)syslog下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

【Unity优化】图形优化系列02:CPU优化

CPU在渲染管线中的工作 1)检测需要被绘制的对象 2)准备发送给GPU的指令 3)向GPU发送指令 多线程渲染 0)Unity 包含三种线程类型:主线程、渲染线程、工作线程 1)主线程:大部分的CPU工作,和部分渲染任务 2)渲染线程:专门用于CPU向GPU发送指令 3)工作线程:执行单独的任务(比如剔除、网格蒙皮) 4)不是所有平台都支持多线程渲染 多...

【WPF学习】第五十六章 基于帧的动画

  除基于属性的动画系统外,WPF提供了一种创建基于帧的动画的方法,这种方法只使用代码。需要做的全部工作是响应静态的CompositionTarge.Rendering事件,触发该事件是为了给每帧获取内容。这是一种非常低级的方法,除非使用标准的基于属性的动画模型不能满足需要(例如,构建简单的侧边滚动游戏、创建基于物理的动画式构建粒子效果模型(如火焰、雪花以...

WPF 客户端浏览器 添加Loading加载进度

在windows开发界面时,使用浏览器来请求和显示网页内容,是比较常见的。 但是在请求网页内容时,因网速或者前端功能复杂加载较慢,亦或者加载时遇到各种问题,如空白/黑屏/加载不完整/证书问题等。 因此需要一个加载进度/加载失败的显示界面。 加载进度显示 界面显示 1. 界面显示,加载进度样式可参考: 绕圈进度条 2. 添加Loading状态枚举。不加载/加...

【转】.NET Core开发日志——IHttpClientFactory

当需要向某特定URL地址发送HTTP请求并得到相应响应时,通常会用到HttpClient类。该类包含了众多有用的方法,可以满足绝大多数的需求。但是如果对其使用不当时,可能会出现意想不到的事情。 博客园官方团队就遇上过这样的问题,国外博主也记录过类似的情况,YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILI...

Linux获取线程tid线程名

Linux获取线程tid线程名 1 2 3 4 5 6 //thread name char cThreadName[32] = {0}; prctl(PR_GET_NAME, (unsigned long)chThreadName); //tid syscall(SYS_gettid)...

web worker 的传值方式以及耗时对比

背景 前一阵子开发的项目 pptx 导入, 由于自己的代码问题,引起了个性能问题,一个 40p 的 pptx 文件,转换成 json 数据,大概要耗时 60s+ ,虽然后面发现是某个使用频率非常高的函数内部,用了 new Function 构造函数 造成的(所以这里顺便提醒一下,如果你很在乎几毫秒的差距的话,建议谨慎使用哈),但是在优化的过程中,一度怀疑是...