Posts Tagged WPF
Đa luồng trong WPF
Đa luồng không phải là một khái niệm mới mẻ hay đặc trưng của WPF mà nó là một tính năng đã được hỗ trợ trong mọi phiên bản của .NET Framework. Tuy vậy, việc áp dụng kĩ thuật đa luồng vào WPF vẫn có những điểm riêng đáng lưu ý.
WPF sử dụng mô hình STA (Single Threaded Apartment) tương tự Windows Form trước đây với các quy tắc cơ bản:
- Các phần tử WPF được sở hữu bởi một luồng duy nhất là luồng tạo ra phần tử đó. Các luồng khác không thể tương tác trực tiếp với phần tử này.
- DispatcherObject là lớp cha của các phần tử WPF, cung cấp các phương thức đảm bảo các đối tượng WPF được sử dụng bởi đúng luồng sở hữu.
Vì sao lại có những hạn chế như vậy? Ban đầu, các nhà thiết kế WPF đã dự định xây dựng một mô hình luồng mới, trong đó các đối tượng giao diện có thể được truy xuất từ bất cứ luồng nào. Tuy nhiên, hướng tiếp cận này gây nên những phức tạp không cần thiết cho các ứng dụng đơn luồng, hơn nữa còn gây khó khăn khi phải giao tiếp với các thư viện cũ. Kết quả là kế hoạch trên đã bị hủy bỏ, và WPF lại quay trở về với mô hình STA như ngày nay.
Điều này có gì quan trọng? Một sai lầm phổ biến của những người mới làm quen với WPF và lập trình đa luồng là cố điều khiển WPF control từ một luồng không sở hữu nó.
private void Button1_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(ChangeText);
thread.Start();
}
private void ChangeText()
{
Thread.Sleep(TimeSpan.FromSeconds(10));
textbox.Text = "Warning! Exception will happen here!";
}
Giả định rằng có một cửa sổ WPF với một button và một textbox. Hàm Button1_Click xử lý sự kiện click chuột trên button, tạo ra một luồng mới và trong luồng đó thay đổi thuộc tính Text của button. Đoạn code trên sẽ gây ra InvalidOperationException. Cách làm đúng trong trường hợp này là sử dụng phương thức BeginInvoke() của DispatcherObject. BeginInvoke có nhiệm vụ điều phối để thực thi đoạn code trong cùng luồng của dispatcher, phương thức này nhận hai tham số: tham số thứ nhất xác định mức độ ưu tiên của tác vụ cần thực hiện, và tham số thứ hai là một delegate trỏ tới hàm thực hiện tác vụ đó. Đoạn code gây lỗi có thể được viết lại như sau:
private void ChangeText()
{
Thread.Sleep(TimeSpan.FromSeconds(10));
// Lấy về dispatcher của cửa sổ hiện tại và thay đổi text cho textbox
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart) delegate()
{
textbox.Text = "Hooray! No error!";
});
}
Lưu ý là DispatcherObject còn có một phương thức Invoke() với cùng tác dụng như trên, khác biệt duy nhất ở chỗ Invoke được thực thi đồng bộ, điều này có nghĩa là luồng gọi Invoke sẽ bị treo đến khi dispatcher thực thi xong tác vụ đó, trong khi lời gọi BeginInvoke() sẽ trả điều khiển ngay lập tức.
Ngoài ra, DispatcherObject cũng cung cấp hai hàm CheckAccess() và VerifyAccess(). CheckAccess trả về true khi đoạn code điều khiển các phần tử WPF được chạy trên đúng luồng, và false nếu ngược lại. Trong khi VerifyAccess() là hàm không trả trị, hàm này sẽ phát sinh InvalidOperationException trong trường hợp luồng thực thi là không hợp lệ. Đây cũng chính là hàm được WPF sử dụng để đảm bảo chúng ta không thao tác với các phần tử WPF từ các luồng không được phép.
Đơn giản hóa quản lý luồng với BackgroundWorker
Trong các ví dụ trước, chúng ta đã tạo và sử dụng luồng tường minh. Cách làm này có ưu điểm là sự linh động tối đa (không hạn chế số luồng được tạo, quản lý độ ưu tiên, trạng thái luồng…). Tuy nhiên trong nhiều trường hợp, chúng ta chỉ cần một giải pháp đơn giản nhất, và đó là nơi mà BackgroundWorker phát huy tác dụng. Về cơ bản, BackgroundWorker hoạt động trên một luồng riêng trong khi ẩn đi các chi tiết phức tạp của lập trình đa luồng. BackgroundWorker cung cấp các sự kiện:
- DoWork: được kích hoạt khi BackgroundWorker bắt đầu thực thi, ta đặt đoạn code cần xử lý bất đồng bộ vào trong hàm xử lý sự kiện này.
- RunWorkerCompleted: khi hoàn tất, BackgroundWorker phát sinh sự kiện này trên luồng của dispatcher, đây là nơi nhận kết quả xử lý và cập nhật các phần tử giao diện.
Sau đây là ví dụ trên viết lại dùng BackgroundWorker. BeginInvoke() bây giờ không còn cần thiết nữa, vì BackgroundWorker đã đảm nhận tất cả:
private void Button1_Click(object sender, RoutedEventArgs e)
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(DoSomething);
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(SomethingDone);
worker.RunWorkerAsync();
}
private void DoSomething(object sender, DoWorkEventArgs e)
{
Thread.Sleep(10000);
}
private void SomethingDone(object sender, RunWorkerCompletedEventArgs e)
{
textbox.Text = "Hooray! No error!";
}
Hàm Button1_Click tạo đối tượng BackgroundWorker, gắn các hàm xử lý sự kiện, và sau đó thực thi BackgroundWorker theo kiểu bất đồng bộ bằng phương thức RunWorkerAsync (lưu ý là bạn có thể cần thêm khai báo using cho namespace System.ComponentModel). Bên cạnh hai sự kiện phổ biến DoWork và RunWorkerCompleted, BackgroundWorker còn cung cấp hai sự kiện khác là Disposed và ProgressChanged, chúng ta có thể bắt các sự kiện này để xử lý thêm khi có nhu cầu. Một điều thú vị cuối cùng là trong WPF, BackgroundWorker có thể được khai báo trực tiếp trong XAML thay vì phải viết code:
<Window.Resources>
<cm:BackgroundWorker x:Key="backgroundWorker" DoWork="DoSomething" RunWorkerCompleted="SomethingDone">
</cm:BackgroundWorker>
</Window.Resources>
Trong khuôn khổ giới hạn, bài viết chỉ trình bày một phần nhỏ về kĩ thuật lập trình đa luồng với WPF. Lập trình là một công việc luôn luôn mới mẻ và thú vị, chắc chắn vẫn còn rất nhiều vấn đề đang chờ bạn khám phá. Chúc các bạn thành công!
Kết hợp WPF và Windows Form
WPF là công nghệ mới rất hấp dẫn đối với các nhà lập trình giao diện. So với Windows Form, WPF cung cấp nhiều tính năng đồ họa và multimedia vượt trội. Trong một ứng dụng lý tưởng, chúng ta có thể quên hẳn Windows Form để tập trung vào WPF. Tuy nhiên trong thực tế, Windows Form vẫn chưa chết. Vì sao?
Thứ nhất, Windows Form là một công nghệ ra đời đã khá lâu và ổn định. Đa số các ứng dụng desktop hiện nay đều được xây dựng trên Windows Form. Trong khi đó, WPF chỉ mới xuất hiện được vài năm và nhiều lập trình viên vẫn quen thuộc với Windows Form hơn là WPF.
Thứ hai, WPF dù có nhiều ưu điểm nhưng vẫn chưa thực sự hoàn chỉnh. Một số tính năng của Windows Form (chẳng hạn một số loại control và hộp thoại) hiện vẫn chưa thể tìm thấy trong WPF. Trong những trường hợp đó, sử dụng lại các thành phần của Windows Form cho ứng dụng WPF (thay vì phát triển lại WPF control từ đầu) có thể sẽ là một tùy chọn đáng cân nhắc.
Từ những lý do trên, có thể thấy rằng Windows Form sẽ chưa biến mất ngay. Thay vào đó, các ứng dụng Windows Form có sẵn sẽ được nâng cấp từ từ sang WPF. Điều này làm nảy sinh nhu cầu kết hợp WPF và Windows Form trong cùng một chương trình. Thật may, cả hai công nghệ này có thể sống chung một cách khá dễ dàng! Bài viết này sẽ trình bày một số phương pháp để giải quyết vấn đề trên:
1. Đặt các WPF control và Windows Form control trong các cửa sổ riêng:
Đây là hướng tiếp cận đơn giản nhất, trong đó mỗi cửa sổ sẽ chỉ chứa một loại control (WPF control hoặc Windows Form control). Ta có thể thêm một cửa sổ Windows Form vào một ứng dụng WPF tương tự như đối với một ứng dụng Windows Form thông thường bằng cách click chuột phải vào tên project trong solution explorer, chọn Add -> New Item, sau đó chọn Windows Form trong Windows Form category, đặt tên và click Add.
Ngược lại, thêm một cửa sổ WPF vào một ứng dụng Windows Form hơi rắc rối hơn một chút vì Visual Studio không cung cấp tùy chọn này trong hộp thoại Add New Item. Tuy nhiên, ta có thể import một cửa sổ WPF có sẵn trong một project khác bằng cách chọn Add -> Existing Item và add cả hai file .xaml và .cs của cửa sổ đó. Với cách này, bạn sẽ phải tự add reference tới ba assembly sau: PresentationCore.dll, PresentationFramework.dll và WindowsBase.dll.
Visual Studio sẽ cung cấp môi trường làm việc phù hợp (trình thiết kế, intellisense…) cho mỗi loại cửa sổ. Bạn có thể thiết kế và biên dịch chương trình một cách tự nhiên như trong bất kì ứng dụng WPF hoặc Windows Form nào. Một lưu ý duy nhất là khi cần show một cửa sổ WPF ở dạng modeless từ trong một cửa sổ Windows Form (hoặc ngược lại), cần gọi phương thức EnableModelessKeyboardInterop (hoặc EnableWindowsFormInterop) để đảm bảo cửa sổ modeless có thể nhận keyboard input bình thường. Ví dụ trong trường hợp cửa sổ modeless WPF:
MyWindow window = new MyWindow();
ElementHost.EnableModelessKeyboardInterop(window);
window.Show();
Bạn có thể sẽ cần add reference đến assembly WindowsFormIntegration.dll để sử dụng lớp ElementHost.
2. Đặt WPF control vào cửa sổ Windows Form:
Trong nhiều trường hợp chúng ta buộc phải sử dụng các WPF control và Windows Form control trong cùng một cửa sổ. Lớp ElementHost cho phép đặt các WPF control trong cửa sổ Windows Form. Chỉ cần tạo WPF control mong muốn và đặt nó vào trong ElementHost, sau đó thêm ElementHost vào tập các control của form.
ElementHost host = new ElementHost();
System.Windows.Controls.DatePicker wpfDatePicker = new System.Windows.Controls.DatePicker();
host.Dock = DockStyle.Fill;
host.Child = wpfDatePicker;
this.Controls.Add(host);
3. Đặt Windows Form control vào cửa sổ WPF:
WPF cung cấp lớp WindowsFormHost có thể chứa một Windows Form control trong property Child. Ta có thể sử dụng lớp này theo kiểu khai báo trong XAML hoặc bằng lập trình:
<Grid>
<WindowsFormsHost>
<wf:TextBox x:Name="txtWinForm"/>
</WindowsFormsHost>
</Grid>
để sử dụng được tag <WindowsFormHost> như trên cần thêm dòng khai báo sau vào tag <Window>:
xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
Còn đây là đoạn code để thêm Windows Form control vào cửa sổ WPF bằng C#:
System.Windows.Forms.Integration.WindowsFormsHost host = new System.Windows.Forms.Integration.WindowsFormsHost();
// Create the ListBox control.
System.Windows.Forms.ListBox lstBox = new System.Windows.Forms.ListBox();
lstBox.Items.Add("Item 1");
lstBox.Items.Add("Item 2");
// Assign the ListBox control as the host control's child.
host.Child = lstBox;
// Add the interop host control to the Grid
this.MyGrid.Children.Add(host);
Nhớ rằng bạn cần Add Reference tới assembly System.Windows.Form và WindowsFormIntegration để đoạn code trên hoạt động.
4. Một vài hạn chế khi sử dụng WPF control và Windows Form control trong cùng một cửa sổ:
- Trong nhiều trường hợp, các control của Windows Form không thể thay đổi kích thước hoặc chỉ có thể thay đổi theo một chiều nhất định.
- Các control của Windows Form cũng không thể được xoay hoặc làm nghiêng. Nếu bị áp dụng các hiệu ứng này, WindowsFormHost sẽ phát sinh sự kiện LayoutError, và cuối cùng sẽ gây InvalidOperationException nếu không được xử lý.