Loading panel control for asynchronous requests

by Tom 29. November 2008 02:56

Wow. just realising that finding the time to write a blog is hard! I've had a busy couple of weeks, and with a big deadline looming in February I think time is only going to become more scarce. It's a good thing I really enjoy coding in Silverlight - unlike some other technologies I've been using lately - and being on the crest of this wave of Silverlight enthusiasm going around at the moment.

Last night I witnessed the first ever meeting of the Silverlight Designer and Developer Network - a new Silverlight user group here in Melbourne. Turnout was huge for a first time and the quality of the presented material was great. In particular, I have to say I was blown away by Jonas Follesø's presentation (a Norwegian Microsoft guy). Not only did he turn out *lots* of code by hand by he actually covered some really cool topics (which I didn't expect at this sort of event). He covered the Silverlight Toolkit (nothing new here), Live Mesh & Silverlight outside the browser (wow! this one got the gears of my brain turning) as well as the as-yet nameless Silverlight Business FX Framework which I'm thinking will be part of Silverlight 3 (we could take weeks/months off our project if this was available now!). So I walked away pretty happy and very positive about putting myself in the Silverlight camp for the foreseeable future.

 

Better get to some code....

A very quick post today. In the course of today I realised needed to unify our approach to managing the UI during asynchronous loads. Basically, the scenario is where you have a control that displays some piece of data that needs to be fetched, and the fetching may take several seconds (or more). Obviously you need to block any user interaction with the control before it is fully loaded, and to follow the convention, you should probably also throw a translucent layer over the control and display a snazzy loading graphic. And of course, a major consideration for me was to make a control I could re-use throughout my app.

Here's what I'm making:

04-01

The way I decided to approach this was to make a very minor extension to a ContentControl - adding an extra piece of content to hold the 'waiting' layer, and an IsLoading property to manage the visual state of the control (i.e. loading or normal). To manage the different visual states, Silverlight provides the handy Visual State Manager. Using the VSM, you can define the visual appearance of your control when it is in certain states (or combinations of states). The Loading Panel can only ever be in two states: Normal and Loading. When the control is in the loading state, the 'waiting' layer has it's visibility set to visible.

Here's the XAML:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
    xmlns:loc="clr-namespace:LoadingPanel"
    >
    <Style TargetType="loc:LoadingPanel">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="loc:LoadingPanel">
                    <Grid>
                        <vsm:VisualStateManager.VisualStateGroups>
                            <vsm:VisualStateGroup x:Name="CommonStates">
                                <vsm:VisualState x:Name="Loading">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames 
                                            Storyboard.TargetName="frameworkelementLoadingContent" 
                                            Storyboard.TargetProperty="Visibility" Duration="0">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </vsm:VisualState>
                                <vsm:VisualState x:Name="Normal"/>
                            </vsm:VisualStateGroup>
                        </vsm:VisualStateManager.VisualStateGroups>
                        <ContentPresenter Content="{TemplateBinding Content}"/>
                        <ContentPresenter x:Name="frameworkelementLoadingContent" 
                                          Content="{TemplateBinding LoadingContent}"
                                          Visibility="Collapsed"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>  

There's little that needs explaining here, since the control comprises little more than two ContentPresenters to hold the pieces of content. The VSM also enables you to se transitions between states, but since our designers haven't asked for anything I've purposely left this out.

In our case, when the control is put into 'Loading' mode, there's a basic Storyboard with a single animation occurring after 0 seconds (the change from collapsed to visible) targeted at the 'waiting' content presenter. This particular control uses only one VisualStateGroup, but with more complex controls, you can use several of these to describe different sets of visual characteristics that may or may not be valid in conjunction with other visual states.

The LoadingPanel.cs is a little more interesting. Here's the code:

using System;
using System.Windows;
using System.Windows.Controls;
 
namespace LoadingPanel
{
    public class LoadingPanel : ContentControl
    {
        public LoadingPanel()
        {
            DefaultStyleKey = typeof(LoadingPanel);
            IsLoading = false;
        }
 
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            GoToState();
        }
 
        private void GoToState()
        {
            if (IsLoading)
            {
                VisualStateManager.GoToState(this, "Loading", false);
            }
            else
            {
                VisualStateManager.GoToState(this, "Normal", false);
            }
        }
 
        public static DependencyProperty IsLoadingProperty =
            DependencyProperty.Register("IsLoading", typeof(bool), typeof(LoadingPanel),
            new PropertyMetadata(new PropertyChangedCallback((d, e) =>
            {
                LoadingPanel lp = d as LoadingPanel;
                lp.GoToState();
            })));
 
        public bool IsLoading
        {
            get { return (bool)GetValue(IsLoadingProperty); }
            set { SetValue(IsLoadingProperty, value); }
        }
 
        public static readonly DependencyProperty LoadingContentProperty =
            DependencyProperty.Register("LoadingContent", typeof(object), typeof(LoadingPanel), null);
 
        public object LoadingContent
        {
            get { return (object)GetValue(LoadingContentProperty); }
            set { SetValue(LoadingContentProperty, value); }
        }
    }
}

 

A few things worth pointing out:

  • The constructor is useful for setting default values for the control's properties
  • The GotoState() method is, by convention, a helper method that interacts with the VisualStateManager
  • I haven't touched the Control Contract at all. This is a series of class Attributes that describe the various parts and states of the control - consumed by Blend. If you don't plan to use the control in Blend then there's  no need for a Control Contract, but apparently it's good practice to leave it in anyway. Karen Corby wrote a good post about this.

Here's how to use the control in your XAML:

<UserControl 
    x:Class="LoadingPanelSL.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:lp="clr-namespace:LoadingPanel;assembly=LoadingPanel"
    >
    <Grid x:Name="LayoutRoot">
        
        <Grid Width="400" Height="400">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
 
            <lp:LoadingPanel IsLoading="{Binding IsLoading}">
                <lp:LoadingPanel.Content>
                    <data:DataGrid x:Name="datagridMyGrid" ItemsSource="{Binding MyData}" />
                </lp:LoadingPanel.Content>
                <lp:LoadingPanel.LoadingContent>
                    <Grid>
                        <Rectangle Fill="LightSlateGray" Opacity="0.3" />
                        <TextBlock Text="Loading..." 
                                   HorizontalAlignment="Center" 
                                   VerticalAlignment="Center" 
                                   Opacity="0.7" FontSize="16" />
                    </Grid>
                </lp:LoadingPanel.LoadingContent>
            </lp:LoadingPanel>
            
            <Button Content="Load Data" x:Name="buttonLoadData" Grid.Row="1" />
        </Grid>
        
    </Grid>
</UserControl>

Note the two attached properties: Content (inherited from ContentControl) and LoadingContent which is a dependency property I added. You should put the data control in the Content tag and the 'waiting' screen in the LoadingContent tag.

The thing that makes this technique very simple to use is being able to databind the IsLoading property. It becomes very simple to flick the IsLoading property from true to false in your presenter and know that the UI side of things is taken care of.

And finally, the code-behind:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.ComponentModel;
 
namespace LoadingPanelSL
{
    public partial class Page : UserControl
    {
        public Page()
        {
            InitializeComponent();
            this.DataContext = new MyDataContext(this);
        }
    }
 
    public class MyDataContext : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private readonly UserControl view;
 
        public MyDataContext(UserControl view)
        {
            this.view = view;
 
            MyDataService service = new MyDataService();
            service.DataLoaded += (object s, MyDataEventArgs e) =>
                {
                    IsLoading = false;
                    MyData = e.Data;
                };
 
            Button b = view.FindName("buttonLoadData") as Button;
            b.Click += (s, e) =>
                {
                    IsLoading = true;
                    service.GetData();
                };
        }
 
        private IEnumerable<string> myData;
        public IEnumerable<string> MyData
        {
            get { return myData; }
            set { myData = value; NotifyChange("MyData"); }
        }
 
        private bool isLoading = false;
        public bool IsLoading
        {
            get { return isLoading; }
            set { isLoading = value; NotifyChange("IsLoading"); }
        }
 
        private void NotifyChange(string name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
 
    public class MyDataService
    {
        public event EventHandler<MyDataEventArgs> DataLoaded;
        private string[] str = ("Microsoft Silverlight extends your existing " +
            "development skills empowering you to build new types of applications " +
            "for the Web regardless of target platform or browser Rapidly create " +
            "compelling rich Web applications using all the familiar features " +
            "languages and tools of the .NET framework").Split();
 
        public void GetData()
        {
            Random rand = new Random();
            Storyboard sb = new Storyboard();
            sb.Duration = new Duration(TimeSpan.FromSeconds(3));
            sb.Completed += new EventHandler((s, e) =>
                {
                    OnDataLoaded(new MyDataEventArgs(from st in str orderby rand.Next() select st));
                });
            sb.Begin();
        }
 
        private void OnDataLoaded(MyDataEventArgs e)
        {
            if (DataLoaded != null) DataLoaded(this, e);
        }
    }
 
    public class MyDataEventArgs : EventArgs
    {
        public IEnumerable<string> Data { get; set; }
        public MyDataEventArgs(IEnumerable<string> data)
        {
            Data = data;
        }
    }
}

 

There's a bit of junk code here to generate 'random' data and simulate an asynchronous request. Nevertheless, the code flow is somewhat similar to the way you find your self working with WCF services in a real LOB app.

If I had time there's any number of improvements I'd make to this, but what sort of software developer has time?  This is exactly the sort of quick and dirty solution that makes Silverlight so fun to work with.

Here's the code.

Categories: Work

Comments

How Win The Lottery
How Win The Lottery United States on 8/2/2009 1:09:27 PM

Does this work on a Mac?

How Win The Lottery
How Win The Lottery United States on 8/4/2009 9:28:44 AM

Makes a lot of sense and thanks for explaining that! I finally get it :o)

Extended Stay San Diego
Extended Stay San Diego United States on 8/4/2009 9:28:50 AM

One keyboard click often leads to another treasure. Great post, man - Thanks

San Diego Real Estate Investing
San Diego Real Estate Investing United States on 8/5/2009 2:25:15 PM

I have been reading a lot on here and have picked up some great ideas. I am trying a couple of ideas at the moment.

Learn Master Guitar
Learn Master Guitar United States on 8/5/2009 2:25:20 PM

Thanks, you cleared up some things for me.

payday loans
payday loans United States on 9/21/2009 1:20:04 PM

We are a group of volunteers and starting a new initiative in a community. Your blog provided us valuable information to work on.You have done a marvellous job!

cash loans
cash loans United States on 10/17/2009 12:32:09 PM

Nice post . keep up the good work

Computer Repair San Marcos CA
Computer Repair San Marcos CA United States on 10/20/2009 11:02:19 PM

Just a big thanks for this great post - I don�t think I could say anything else that would do it justice.

Computer Repair San Marcos CA
Computer Repair San Marcos CA United States on 10/20/2009 11:02:23 PM

One mouse click often leads to another fine blog. Like yours - Thanks.

ATV San Diego
ATV San Diego United States on 10/21/2009 2:25:22 AM

I am thankful for the new things I learned reading your post. Thanks.

Computer Repair San Marcos CA
Computer Repair San Marcos CA United States on 10/21/2009 2:25:26 AM

I hadn�t thought of this before, but it�s definitely an interesting idea. Thanks for the insight.

scratch and dent
scratch and dent United States on 10/25/2009 5:53:55 PM

good post Thank for sharing

easy personal loans
easy personal loans United States on 11/4/2009 12:22:53 PM

Nice resource. rss feed added

payday loans
payday loans United States on 11/7/2009 10:46:12 AM

I like what I see. keep it going

Sears Parts
Sears Parts United States on 12/7/2009 8:40:05 PM

Vielen Dank für eine gute post

payday loans
payday loans United States on 12/20/2009 7:30:41 PM

Nice post . keep up the good work

cash today
cash today United States on 1/12/2010 4:56:58 PM

Well my friens .. as I always say ... Be polite to all, but intimate with few. Nice post.

payday loans
payday loans United States on 1/20/2010 2:42:01 AM

Being at the right place at the right time can only happen when you keep moving toward the next opportunity.

Loans in SD
Loans in SD United States on 1/21/2010 1:31:00 PM

It is good to have an end to journey towards, but it is the journey that matters, in the end.

payday loans
payday loans United States on 1/23/2010 12:21:19 PM

A leader is best when people barely know he exists, when his work is done, his aim fulfilled, they will say: we did it ourselves.

instant loans
instant loans United States on 1/30/2010 3:55:26 AM

Whatever is worth doing at all is worth doing well.

payday loans
payday loans United States on 2/5/2010 9:21:24 PM

The world leaders in innovation and creativity will also be world leaders in everything else.

Buenos Aires travel
Buenos Aires travel United States on 2/11/2010 3:21:45 PM

You have a nice blog. I will be visiting this blog very often.

helm seats
helm seats United States on 2/15/2010 9:27:41 AM

Your post related to silverlight is really very well written.Keep sharing more knowledgeable posts like this.

alta white teeth whitening
alta white teeth whitening United States on 2/15/2010 11:49:24 PM

Don't let yourself be lulled into inaction.

very bad credit loans
very bad credit loans United States on 3/6/2010 1:13:22 PM

i am looking forward for you your next post

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading