Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keeping a WPF UI responsive while showing a slow loading UserControl

Tags:

mvvm

wpf

We have a WPF application written using the MVVM pattern. Within the application is a TabControl with different UserControls within each tab. Under certain conditions one of the UserControls on a tab can take a significant portion of time to load when switching to the containing tab.

This is NOT because of any performance bottlenecks in the ViewModel. But instead, is due to significant amount of time that the usercontrol takes to bind to the ViewModel, and to create the various UI elements contained within it and initialize them.

When the user clicks on the tab for this usercontrol, the UI becomes completely unresponsive until the control has completed loading. If fact you don't even see the "active tab" switch until everything is loaded.

What strategies could I use to display a "spinner" with some sort of "please wait, loading..." message while waiting for the UI elements to complete loading?

UPDATE with sample code:

The below demonstrates the type of problem I am trying to get around. When you click on the "slow tab". The UI becomes unresponsive until all the items in the slow tab have rendered.

In the below, TestVM is the viewmodel for the slow tab. It has a large collection of children objects. Each created with it's own data template.

How could I display a "loading" message while the slow tab finishes loading?

public class MainVM
{
    private TestVM _testVM = new TestVM();
    public TestVM TestVM
    {
        get { return _testVM; }
    }
}

/// <summary>
/// TestVM is the ViewModel for the 'slow tab'. It contains a large collection of children objects that each will use a datatemplate to render. 
/// </summary>
public class TestVM
{
    private IEnumerable<ChildBase> _children;

    public TestVM()
    {
        List<ChildBase> list = new List<ChildBase>();
        for (int i = 0; i < 100; i++)
        {
            if (i % 3 == 0)
            {
                list.Add(new Child1());
            }
            else if (i % 3 == 1)
            {
                list.Add(new Child2());
            }
            else
            {
                list.Add(new Child3());
            }
        }
        _children = list;
    }

    public IEnumerable<ChildBase> Children
    {
        get {  return _children; }
    }
}

/// <summary>
/// Just a base class for a randomly positioned VM
/// </summary>
public abstract class ChildBase
{
    private static Random _rand = new Random(1);

    private int _top = _rand.Next(800);
    private int _left = _rand.Next(800);

    public int Top { get { return _top; } }
    public int Left { get { return _left; } }
}

public class Child1 : ChildBase { }

public class Child2 : ChildBase  { }

public class Child3 : ChildBase { }

<Window x:Class="WpfApplication3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication3"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>

        <!-- Template for the slow loading tab -->
        <DataTemplate DataType="{x:Type local:TestVM}">
            <ItemsControl ItemsSource="{Binding Children}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas IsItemsHost="True"></Canvas>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemContainerStyle>
                    <Style TargetType="FrameworkElement">
                        <Setter Property="Canvas.Top" Value="{Binding Top}"></Setter>
                        <Setter Property="Canvas.Left" Value="{Binding Left}"></Setter>
                    </Style>
                </ItemsControl.ItemContainerStyle>
            </ItemsControl>
        </DataTemplate>

        <!-- examples of different child templates contained in the slow rendering tab -->
        <DataTemplate DataType="{x:Type local:Child1}">
            <DataGrid></DataGrid><!--simply an example of a complex control-->
        </DataTemplate>

        <DataTemplate DataType="{x:Type local:Child2}">
            <RichTextBox Width="30" Height="30">
                <!--simply an example of a complex control-->
            </RichTextBox>
        </DataTemplate>

        <DataTemplate DataType="{x:Type local:Child3}">
            <Calendar Height="10" Width="15"></Calendar>
        </DataTemplate>

    </Window.Resources>
    <Grid>
        <TabControl>
            <TabItem Header="Fast Loading tab">
                <TextBlock Text="Not Much Here"></TextBlock>
            </TabItem>
            <TabItem Header="Slow Tab">
                <ContentControl Content="{Binding TestVM}"></ContentControl>
            </TabItem>
        </TabControl>
    </Grid>
</Window>
like image 274
eoldre Avatar asked Mar 21 '13 18:03

eoldre


1 Answers

What do u need is here

http://msdn.microsoft.com/en-us/library/ms741870.aspx

 public partial class Window1 : Window
    {
        // Delegates to be used in placking jobs onto the Dispatcher. 
        private delegate void NoArgDelegate();
        private delegate void OneArgDelegate(String arg);

        // Storyboards for the animations. 
        private Storyboard showClockFaceStoryboard;
        private Storyboard hideClockFaceStoryboard;
        private Storyboard showWeatherImageStoryboard;
        private Storyboard hideWeatherImageStoryboard;

        public Window1(): base()
        {
            InitializeComponent();
        }  

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Load the storyboard resources.
            showClockFaceStoryboard = 
                (Storyboard)this.Resources["ShowClockFaceStoryboard"];
            hideClockFaceStoryboard = 
                (Storyboard)this.Resources["HideClockFaceStoryboard"];
            showWeatherImageStoryboard = 
                (Storyboard)this.Resources["ShowWeatherImageStoryboard"];
            hideWeatherImageStoryboard = 
                (Storyboard)this.Resources["HideWeatherImageStoryboard"];   
        }

        private void ForecastButtonHandler(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            hideWeatherImageStoryboard.Begin(this);

            // Start fetching the weather forecast asynchronously.
            NoArgDelegate fetcher = new NoArgDelegate(
                this.FetchWeatherFromServer);

            fetcher.BeginInvoke(null, null);
        }

        private void FetchWeatherFromServer()
        {
            // Simulate the delay from network access.
            Thread.Sleep(4000);              

            // Tried and true method for weather forecasting - random numbers.
            Random rand = new Random();
            String weather;

            if (rand.Next(2) == 0)
            {
                weather = "rainy";
            }
            else
            {
                weather = "sunny";
            }

            // Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(
                System.Windows.Threading.DispatcherPriority.Normal,
                new OneArgDelegate(UpdateUserInterface), 
                weather);
        }

        private void UpdateUserInterface(String weather)
        {    
            //Set the weather image 
            if (weather == "sunny")
            {       
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "SunnyImageSource"];
            }
            else if (weather == "rainy")
            {
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "RainingImageSource"];
            }

            //Stop clock animation
            showClockFaceStoryboard.Stop(this);
            hideClockFaceStoryboard.Begin(this);

            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;     
        }

        private void HideClockFaceStoryboard_Completed(object sender,
            EventArgs args)
        {         
            showWeatherImageStoryboard.Begin(this);
        }

        private void HideWeatherImageStoryboard_Completed(object sender,
            EventArgs args)
        {           
            showClockFaceStoryboard.Begin(this, true);
        }        
    }

P.S. Perhaps it is useful as well http://tech.pro/tutorial/662/csharp-tutorial-anonymous-delegates-and-scoping and Make dispatcher example to work

like image 95
Friend Avatar answered Sep 27 '22 17:09

Friend