DispatcherHelper


DispatcherHelper

通常,WPF 應用程序從兩個線程開始:一個用於處理呈現,
一個用於管理 UI。呈現線程有效地隱藏在后台運行,而 UI 線程則接收輸入、處理事件、繪制屏幕
以及運行應用程序代碼。所以我們的大多數操作都會在UI線程中執行,同時它也處理繪制屏幕,如果我們
的一個操作相當耗時,那么它就沒有機會處理繪制屏幕,此時我們是不能夠拖動窗口的,也就是通常
說的屏幕卡住了。

Dispatcher,DispatcherObject,DependencyObject

Dispatcher即調度器,我們可以把UI線程看作CPU,我們的每個操作就是指令,指令發送到CPU處理,
CPU同時只能處理一個指令,當指令非常多時,優先級高的指令要先處理,低的可以稍后處理,Dispatcher
就是用於處理這些操作的,它將我們的操作根據優先級排隊,等待UI線程來處理。

在WPF中,所有的WPF對象都派生自DispatcherObject,
DispatcherObject暴露了Dispatcher屬性用來取得創建對象線程對應的Dispatcher。
鑒於線程親緣性,DispatcherObject對象只能被創建它的線程所訪問,
其他線程修改DispatcherObject需要取得對應的Dispatcher,
調用Invoke或者BeginInvoke來投入任務。
一個UI線程至少有一個Dispatcher來建立消息泵處理任務,一個Dispatcher只能對應一個UI線程。

WPF中的控件都是主UI線程創建的,也就是只有主UI線程可以訪問他們,如果是其他線程訪問控件
那么就會報錯

        Task.Factory.StartNew(() =>
        {
            btn.Content = "HelloWorld";
        });

跨線程錯誤

解決辦法就是在主線程中訪問他們,或是

Task.Factory.StartNew(() =>
        {
            //耗時操作
            Thread.Sleep(5000);

            //最后結果通過主線程更新到控件
            btn.Dispatcher.Invoke(() =>
            {
                btn.Content = "HelloWorld";
            });
            
        });

在WPF中,Dispatcher,DispatcherObject和DependencyObject決定了一個對象的線程親緣性,這里提供一個方便查看源代碼的網址
我們首先查看Dispatcher的源碼,發現兩個比較重要的方法和一個靜態屬性

Dispatcher
|____CurrentDispatcher
|____CheckAccess()
|____VerifyAccess()

CurrentDispatcher獲取當前線程的Dispatcher,如果當前線程沒有Dispatcher,那么就創建一個
CheckAccess()方法用於判斷,當前Dispatcher所屬的線程是不是當前線程
VerifyAccess()其實就是調用的CheckAccess,如果Dispatcher不屬於當前線程,那么就報異常,這就是上面圖片所示的異常

我們在來看DispatcherObject,看到一個構造函數,2個方法和一個屬性

DispatcherObject
|____DispatcherObject
|____Dispatcher
|____CheckAccess()
|____VerifyAccess()

當創建一個DispatcherObject對象的時候,會給這個對象分配一個當前線程的Dispatcher,其他兩個方法也就是對Dispatcher方法的封裝

好,最后我們來看DependencyObject,看到兩個重要的方法GetValue,SetValue

DependencyObject
|____SetValue(DependencyProperty dp,object value)
|____GetValue(DependencyProperty dp)

這兩個方法中的的第一句就是this.VerifyAccess(),而DependencyObject是直接繼承自DispatcherObject的,其實就是使用的Dispatcher的VerifyAccess,到此
WPF對象的線程親緣性已經很明了了,所以,一個對象只能被它所創建的線程所訪問。

Binding的源在多線程中怎么處理

網上比較好的文章

我的理解

我說下我的理解,我們在ViewModel中更新數據都要觸發OnPropertyChanged事件,這個事件其實是被
一個叫做PropertyChangedEventManager的家伙訂閱的,它又會去觸發相應控件的OnPropertyChanged事件
,都是事件,事件也是方法,在哪個線程促發的事件,就在哪個線程處理,照這么說,我們在非UI線程
更新Model的屬性,那么對應的會在非UI線程處理與之相綁定的控件,這里就會報如上的錯誤。我們做個
實驗來測試下是不是這樣的。

實驗

界面上顯示一個Teacher的基本數據,Name,Age,還有所管理的學生Student,我們在非UI線程做
以下操作

  • 修改Teacher的屬性
  • 增加學生數
  • 修改最后一名學生的姓名

Teacher.cs

public class Teacher : ObservableObject
{
    private string _name;
    private int _age;
    private ObservableCollection<Student> _students;

    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
            RaisePropertyChanged(() => Name);
        }
    }

    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            _age = value;
            RaisePropertyChanged(() => Age);
        }
    }

    public ObservableCollection<Student> Students
    {
        get
        {
            return _students;
        }
        set
        {
            _students = value;
            RaisePropertyChanged(() => Students);
        }
    }
}

Student.cs

public class Student : ObservableObject
{
    private string _name;
    private int _age;

    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
            RaisePropertyChanged(() => Name);
        }
    }

    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            _age = value;
            RaisePropertyChanged(() => Name);
        }
    }
}

MainView.xaml

    <Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>

    <StackPanel Grid.Column="0">
        <TextBlock Text="{Binding Teacher.Name}"></TextBlock>
        <TextBlock Text="{Binding Teacher.Age}"></TextBlock>
        <ListView ItemsSource="{Binding Teacher.Students}">
            <ListView.View>
                <GridView>
                    <GridViewColumn DisplayMemberBinding="{Binding Name}"></GridViewColumn>
                    <GridViewColumn DisplayMemberBinding="{Binding Age}"></GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
    </StackPanel>

    <StackPanel Grid.Column="1">
        <Button Content="改變教師名稱" Command="{Binding ChangeTeacherNameCommand}"></Button>
        <Button Content="增加學生" Command="{Binding AddStudentCommand}"></Button>
        <Button Content="改變最后一名學生名稱" Command="{Binding ChangeLastStudentNameCommand}"></Button>
    </StackPanel>
</Grid>

MainViewModel.cs

public class MainViewModel : ViewModelBase
{
    private Teacher _teacher;

    public Teacher Teacher
    {
        get
        {
            return _teacher;
        }
        set
        {
            _teacher = value;
            RaisePropertyChanged(() => Teacher);
        }
    }

    public RelayCommand ChangeTeacherNameCommand
    {
        get; set;
    }

    public RelayCommand AddStudentCommand
    {
        get; set;
    }

    public RelayCommand ChangeLastStudentNameCommand
    {
        get; set;
    }

    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel()
    {
        Teacher = new Teacher()
        {
            Name = "LaoZhao",
            Age = 30,
            Students = new ObservableCollection<Student>()
            {
                new Student()
                {
                    Name="LaoZhange",
                    Age = 18
                }
            }
        };

        InitCommand();
    }

    private void InitCommand()
    {
        ChangeTeacherNameCommand = new RelayCommand(() =>
          {
              Task.Factory.StartNew(() =>
              {
                  Teacher.Name = "MaYun";
              });
          });

        AddStudentCommand = new RelayCommand(() =>
          {
              Task.Factory.StartNew(() =>
              {
                  Teacher.Students.Add(new Student()
                  {
                      Name = "LaoLi",
                      Age = 25
                  });
              });
          });

        ChangeLastStudentNameCommand = new RelayCommand(() =>
          {
              Task.Factory.StartNew(() =>
              {
                  var student = Teacher.Students.LastOrDefault();

                  if (student != null)
                  {
                      student.Name = "TheLast";
                  }
              });
          });
    }
}

最后發現,在非UI線程更新Teacher的姓名和Student的姓名是沒有問題的,那是因為WPF在后台強制
使用了UI線程,然而向集合中增加一個學生卻報錯~(我猜測WPF針對有些控件不會幫我們切換到UI主線程),為了避免這些情況,如果要更新Model的屬性,
請在UI線程中。但是我們都是在ViewModel中,並不知道任何一個控件,自然也沒有辦法拿到Dispatcher
好了,主角終於登場了DispatcherHelper

DispatcherHelper

DispatcherHelper的使用首先要初始化用來保存主線程的Dispatcher,這個Dispatcher即創建界面的
線程,哪個線程創建的控件,只能由那個線程才能訪問。所以我們一般在App的構造函數中初始化DispatcherHelper

    public App()
    {
        DispatcherHelper.Initialize();
    }

DispatcherHelper主要成員以下:

  • UIDispatcher 屬性:當DispatcherHelper調用Initialize方法時,使用當前線程的Dispatcher
  • Initialize 方法:初始化DispatcherHelper,並保存當前線程的Dispatcher
  • CheckBeginInvokeOnUI 方法:如果當前線程為非UI線程,那么在UI線程異步執行該方法,如果為UI
    線程,那么立即執行
  • Reset 方法:重置Dispatcher為Null
  • RunAsync 方法:在UI線程上異步執行

所以,剛才增加學生出錯的代碼,我們可以修改為

    AddStudentCommand = new RelayCommand(() =>
          {
              Task.Factory.StartNew(() =>
              {
                  DispatcherHelper.CheckBeginInvokeOnUI(() =>
                  {
                      Teacher.Students.Add(new Student()
                      {
                          Name = "LaoLi",
                          Age = 25
                      });
                  });
              });
          });

當然,直接調用那是最好,這里只是模擬非UI線程調用的情況。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



猜您在找
 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM