Introduction
In this article we will see how to create responsive UI in WPF. A very simple example.
Background
In our application we often face situations where we need to communicate with a service to get data. Or we need to do some I/O operation and the get the data after processing it. So what happens if the service is slow or even if the amount of data is huge it takes time to populate the data. In the mean time the UI kind of freezes. And we become confused whether the application is running or not. The operating system shows it "Not Responding". Where as actually your application is waiting for some data and not crashed. Here in this article we will see how we can avoid this kind of situation and get rid off confusion.
Behind the Scenes
First of all we need to understand what happens when we ask for some data or want to do any kind of operation that has some impact on UI. Every UI in WPF runs on a thread and managed by that thread. That thread is responsible for managing and updating the UI. The point is whenever we are doing any kind of update or any kind of UI related work we need to be on that thread. Cause any other thread does not has the access to work on the UI. In most of the cases WPF does that for us automatically. That's why we don't think about that. But in certain scenarios like mentioned above we need to manage this by ourselves. So the point is UI related work runs on a thread, I/O related work should run on different thread and so on. And if we don't follow this things may screw up.
Let's think of scenario similar to the one we are trying to work on. Suppose we have a class that returns a calculation result of any particular thing. And we need to get that result to show it in the UI. In an ideal scenario we would use a service to call that class and get the result. But just for the simplicity of this example we will call this class directly from our code behind. Same I have another class that returns a name which is also similar like the previous one. And I have another class which is not in any service and returns a age value.
- So in first two cases we need to get data from services (though we are not using any). The point is that here we may have a time delay factor. Cause can be different, like:
- Network congestion
- Busy Server
- Packet Loss
- Round Trip Time
- In the third case there is no time factor.
Now let's go through this scenarios in both ways (wrong and right).
The Wrong Way
First of all let's see how the UI looks like. So simple..
<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>
So we have a StackPanel
where we can see that we have 3 textblocks and 3 buttons. Now Let's see the code behind of this XAML.
public partial class Wrong : Window
{
public Wrong()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
CalBill obBill = new CalBill();
TBBill.Text = obBill.GenerateBill().ToString();
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Name obNam = new Name();
TBName.Text = obNam.ReturnName();
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
Age obAge = new Age();
TBAge.Text = obAge.ReturnAge().ToString();
}
}
Now run this application and click on the Get Bill or Get Name button. What is happening?? It seems that the UI has been frozen or kind of not responding. But after a certain time we can see the result appear in the text block above the button. But in the mean time we can't do anything in the UI. Now why is that? Because whatever we are doing under the button click event, we are doing it in the UI thread. So the tread becomes busy to do that work (in this case lets' just say it becomes busy to get data from the service as we said before) and therefore the user interface freezes.
Right Way
So what is the solution. Well like I said before, any kind of time consuming work should run on a different thread. And that thread should do all the waiting stuffs. And once it gets the data then it will give it back to the UI. And for this we can use the ThreadPool
. So we can use the ThreadPool
to call the service or method and queue it in the Work item of ThreadPool
. So that the UI thread becomes free and ThreadPool
will take care of those issues like calling service and getting the data from the service or may be wait fro the service to respond. And therefore UI does not freeze anymore and we don't get confused too. And the code for that looks like this:
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();
}
Check we are queuing the GetCal
function for execution. The method executes when a thread of that ThreadPool
becomes available. So now the time consuming work is handled in a different thread. And therefore the UI is free.
Remember I said on top of this article that each UI in WPF runs on a thread and managed by that thread only. Dispatcher
is that thread. And in the GetCal
method we called Dispatcher.BeginInvoke()
method where we created a delegate to update the UI with return result. Now what would happen if don't call Dispatcher.BeginInvoke()
method and call the UpdateUI
method directly. Something similar like this:
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);
}
If we run it like this we will get a error. That actually says that the current thread does not has the permission to update the UI or work on the UI. Because now we are trying to update UI from a different thread rather than the thread which has the access to do it and that is the dispatcher thread.
So now we understand that how we can keep the UI responsive. Let's now see the example based on the three scenarios stated above. First a class that returns a calculation and another class that returns a name. And both this classes take time to return data.
public class CalBill
{
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
{
public string ReturnName()
{
string name = "John Here";
Thread.Sleep(5000);
return name;
}
}
public class Age
{
public int ReturnAge()
{
return 100;
}
}
The code behind file of XAML.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void btnBill_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();
}
private void btnName_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(GetName);
}
private void GetName(object state)
{
string name = " ";
Name obName = new Name();
name = obName.ReturnName();
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();
}
}
Now if we run this application we will see a very responsive UI. No more confusion or waiting and happy working..
There are other ways of doing it too. In the next article we will see another way of doing this.