WPF 响应式 UI 简单示例






4.62/5 (10投票s)
响应式 UI。
引言
在本文中,我们将了解如何在 WPF 中创建响应式 UI。一个非常简单的例子。
背景
在我们的应用程序中,我们经常会遇到需要与服务通信以获取数据的情况。或者我们需要执行一些 I/O 操作,然后在处理数据后获取它。那么,如果服务很慢,或者即使数据量很大需要很长时间才能填充怎么办? 在此期间,UI 会变得 冻结。然后我们就会感到困惑,应用程序是否仍在运行。操作系统会显示“未响应”。而实际上,您的应用程序正在等待数据,并没有崩溃。在这里,在本文中,我们将看到如何避免这种情况,摆脱困惑。
幕后
首先,我们需要了解当我们请求某些数据或想要执行任何可能影响 UI 的操作时会发生什么。 WPF 中的每个 UI 都在一个线程上运行,并由该线程管理。该线程负责管理和更新 UI。关键是,当我们进行任何更新或任何 UI 相关工作时,我们都需要在该线程上。因为其他线程无权操作 UI。在大多数情况下,WPF 会自动为我们处理。这就是为什么我们不考虑它。但在某些情况下,如上所述,我们需要自己管理。所以关键是 UI 相关工作在一个线程上运行,I/O 相关工作应该在不同的线程上运行,等等。 如果我们不遵循这些,可能会搞砸。
让我们考虑一个与我们正在尝试处理的场景相似的场景。 假设我们有一个类,它返回某个事物的计算结果。我们需要获取该结果以在 UI 中显示它。在理想情况下,我们会使用服务调用该类并获取结果。但为了使此示例简单化,我们将直接从代码隐藏中调用该类。同样,我还有一个返回姓名的类,它也与 前面的类类似。还有一个类不在任何服务中,它返回年龄值。
- 所以在前两种情况下,我们需要从服务获取数据(尽管我们没有使用任何服务)。关键是这里可能有时间延迟因素。原因可能不同,例如:
- 网络拥塞
- 服务器繁忙
- 数据包丢失
- 往返时间
- 第三种情况没有时间因素。
错误的方式
首先,让我们看看 UI 是什么样的。就是这么简单。
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Left">
<TextBlock x:Name="TBBill"/>
<Button Content="Get Bill" Click="Button_Click"/>
<TextBlock x:Name="TBName"/>
<Button Content="Get Name" Click="Button_Click_1"/>
<TextBlock x:Name="TBAge"/>
<Button Content="Get Age" Click="Button_Click_2"/>
</StackPanel>
所以我们有一个 StackPanel
,我们可以看到它有 3 个文本块和 3 个按钮。现在让我们看看这个 XAML 的代码隐藏。
/// <summary>
/// Interaction logic for Wrong.xaml
/// </summary>
public partial class Wrong : Window
{
public Wrong()
{
InitializeComponent();
}
/// <summary>
/// This function gets the bill. Instead of calling the service we are
/// calling the class directly. Time delay has been created in the class
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Button_Click(object sender, RoutedEventArgs e)
{
CalBill obBill = new CalBill();
TBBill.Text = obBill.GenerateBill().ToString();
}
/// <summary>
/// This function gets the name. Instead of calling the service we are
/// calling the class directly. Time delay has been created in the class
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Name obNam = new Name();
TBName.Text = obNam.ReturnName();
}
/// <summary>
/// Gets the age. No service scenario here as discussed above.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Button_Click_2(object sender, RoutedEventArgs e)
{
Age obAge = new Age();
TBAge.Text = obAge.ReturnAge().ToString();
}
}
现在运行此应用程序并单击“获取账单”或“获取姓名”按钮。发生了什么??UI 似乎 冻结 了,或者有点无响应。但过了一段时间,我们可以在按钮上方的文本块中看到结果。但在此期间,我们无法在 UI 中执行任何操作。为什么会这样?因为我们在按钮点击事件下所做的一切,都是在 UI 线程上进行的。所以线程忙于完成这项工作(在本例中,让我们假设它忙于从服务获取数据,就像我们之前说的那样),因此用户界面冻结了。
正确的方式
那么解决方案是什么?嗯,正如我之前所说,任何耗时的操作都应该在不同的线程上运行。然后那个线程应该做所有的等待工作。一旦它获取到数据,它就会将其返回给 UI。 为此,我们可以使用 ThreadPool
。所以我们可以使用 ThreadPool
来调用服务或方法,并将其排队到 ThreadPool
的工作项中。这样 UI 线程就可以释放, ThreadPool
将负责处理这些问题,例如调用服务、从 服务获取数据,或者可能等待服务响应。因此,UI 不再冻结,我们也不会感到困惑。代码如下:
private void Button_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(GetCal);
}
private void GetCal(object state)
{
int totalbill = 0;
CalBill obBill = new CalBill();
totalbill = obBill.GenerateBill();
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action<int>(UpdateUI), totalbill);
}
private void UpdateUI(int bill)
{
TBBill.Text = "Total Bill: " + bill.ToString();
}
检查我们正在 排队 执行GetCal
函数。当ThreadPool
的某个线程可用时,该方法就会执行。所以现在耗时的操作在不同的线程上处理。因此,UI 是免费的。
请记住,我在本文开头说过,WPF 中的每个 UI 都在一个线程上运行,并且仅由该线程管理。 Dispatcher
就是那个线程。在 GetCal
方法中,我们调用了 Dispatcher.BeginInvoke()
方法,在那里我们创建了一个委托来使用返回的结果更新 UI。 现在,如果我们不调用 Dispatcher.BeginInvoke()
方法而直接调用 UpdateUI
方法,会发生什么? 有点像这样:
private void Button_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(GetCal);
}
private void GetCal(object state)
{
int totalbill = 0;
CalBill obBill = new CalBill();
totalbill = obBill.GenerateBill();
UpdateUI(totalbill);
}
如果我们这样运行,我们会收到一个错误。该错误实际上表明当前线程没有权限更新 UI 或在 UI 上工作。因为现在 我们正试图从一个与具有访问权限的调度程序线程不同的线程更新 UI。
所以现在我们明白了如何保持 UI 的响应性。 现在让我们根据上面提到的三个场景来看例子。第一个类返回计算结果,另一个类返回姓名。这两个类都需要一些时间来返回数据。
public class CalBill
{
// Multiple lopps with huge iteration has been created
// intentionally just to create time delay.
public int GenerateBill()
{
int totalBill = 0;
for (int i = 0; i < 1000; i++)
{
for (int j = 1; j < 1001; j++)
{
for (int k = 1; k < 1802; k++)
{
totalBill = i + j + k;
}
}
}
return totalBill;
}
}
public class Name
{
//Thread.sleep has been used just to delay some time.
public string ReturnName()
{
string name = "John Here";
Thread.Sleep(5000);
return name;
}
}
public class Age
{
public int ReturnAge()
{
return 100;
}
}
XAML 的代码隐藏文件。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
/// <summary>
/// Queuing the GetCal method for execution which is managed by ThreadPool
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnBill_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(GetCal);
}
/// <summary>
/// This function gets the bill. Instead of calling the service we are
/// calling the class directly. Time delay has been created in the class
/// </summary>
/// <param name="state"></param>
private void GetCal(object state)
{
int totalbill = 0;
CalBill obBill = new CalBill();
totalbill = obBill.GenerateBill();
// Calls the dispatcher thread of the UI
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action<int>(UpdateUI), totalbill);
}
private void UpdateUI(int bill)
{
TBBill.Text = "Total Bill" + bill.ToString();
}
/// <summary>
/// Queuing the GetCal method for execution which is managed by ThreadPool
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnName_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(GetName);
}
/// <summary>
/// This function gets the name. Instead of calling the service we are
/// calling the class directly. Time delay has been created in the class
/// </summary>
/// <param name="state"></param>
private void GetName(object state)
{
string name = " ";
Name obName = new Name();
name = obName.ReturnName();
// Calls the dispatcher thread of the UI
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action<string>(UpdateUI), name);
}
private void UpdateUI(string name)
{
TBName.Text = "Name is " + name;
}
private void btnAge_Click(object sender, RoutedEventArgs e)
{
Age obAge = new Age();
TBAge.Text = obAge.ReturnAge().ToString();
}
}
现在如果我们运行这个应用程序,我们将看到一个非常响应式的 UI。不再有困惑或等待,工作愉快。
还有其他方法可以做到这一点。在下一篇文章中,我们将看到另一种方法。