65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 响应式 UI 简单示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (10投票s)

2013 年 4 月 30 日

CPOL

5分钟阅读

viewsIcon

54991

downloadIcon

2487

响应式 UI。

引言

在本文中,我们将了解如何在 WPF 中创建响应式 UI。一个非常简单的例子。

背景

在我们的应用程序中,我们经常会遇到需要与服务通信以获取数据的情况。或者我们需要执行一些 I/O 操作,然后在处理数据后获取它。那么,如果服务很慢,或者即使数据量很大需要很长时间才能填充怎么办?  在此期间,UI 会变得  冻结。然后我们就会感到困惑,应用程序是否仍在运行。操作系统会显示“未响应”。而实际上,您的应用程序正在等待数据,并没有崩溃。在这里,在本文中,我们将看到如何避免这种情况,摆脱困惑。

幕后

首先,我们需要了解当我们请求某些数据或想要执行任何可能影响 UI 的操作时会发生什么。 WPF 中的每个 UI 都在一个线程上运行,并由该线程管理。该线程负责管理和更新 UI。关键是,当我们进行任何更新或任何 UI 相关工作时,我们都需要在该线程上。因为其他线程无权操作 UI。在大多数情况下,WPF 会自动为我们处理。这就是为什么我们不考虑它。但在某些情况下,如上所述,我们需要自己管理。所以关键是 UI 相关工作在一个线程上运行,I/O 相关工作应该在不同的线程上运行,等等。 如果我们不遵循这些,可能会搞砸。

让我们考虑一个与我们正在尝试处理的场景相似的场景。 假设我们有一个类,它返回某个事物的计算结果。我们需要获取该结果以在 UI 中显示它。在理想情况下,我们会使用服务调用该类并获取结果。但为了使此示例简单化,我们将直接从代码隐藏中调用该类。同样,我还有一个返回姓名的类,它也与  前面的类类似。还有一个类不在任何服务中,它返回年龄值。

  1. 所以在前两种情况下,我们需要从服务获取数据(尽管我们没有使用任何服务)。关键是这里可能有时间延迟因素。原因可能不同,例如:   
    1. 网络拥塞
    2. 服务器繁忙
    3. 数据包丢失
    4. 往返时间
  2. 第三种情况没有时间因素。
现在让我们分别通过两种方式(错误和正确)来看待这些场景。 

 错误的方式  

 首先,让我们看看 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。不再有困惑或等待,工作愉快。

还有其他方法可以做到这一点。在下一篇文章中,我们将看到另一种方法。

© . All rights reserved.