With C# 5 (.NET 4.5), it became a lot easier to create asynchronous methods. There’s also a great MSDN article on how to leverage this using MVVM so that you can have properties update the view when they’ve finished loading. What I wanted was a simple control that would indicate to the user that the content was being fetched (via binding to the NotifyTaskCompletion.IsNotCompleted
property).
What I wanted though was a simple attached property on a ContentControl
(or HubSection
in my case but it’s easy to modify for other control types) and it would switch to the loading indicator based on the bound value, as I didn’t want to have to modify lots of DataTemplate
s to add the indicator to them and then mess around with the visibility of the content/indicator based on if the value was being loaded or not.
The actual visual part of the control is straight forward:
public sealed class AsyncIndicator : Control
{
protected override Size MeasureOverride(Size availableSize)
{
Size desiredSize = base.MeasureOverride(availableSize);
return new Size(
GetSize(availableSize.Width, desiredSize.Width),
GetSize(availableSize.Height, desiredSize.Height));
}
private static double GetSize(double available, double desired)
{
return double.IsPositiveInfinity(available) ? desired : available;
}
}
All this does is make the control take all the available size of the parent, taking into account if the parent provides us with an infinite amount of space (e.g. StackPanel
). Here’s the simple template for it, which I tuck away inside Generic.xaml:
<Style TargetType="controls:AsyncIndicator">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:AsyncIndicator">
<Border Background="#33888888">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<ProgressRing HorizontalAlignment="Center"
IsActive="True" />
<TextBlock Style="{ThemeResource BodyTextBlockStyle}"
Text="Loading" />
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Like I said, nothing complex about this nor nothing that would really warrant a new control; it’s just a spinning progress (new to WinRT) with a label. However, I’m also going to store the attached property in this control. How the attached property works is when it’s set to true
, it will replace the ContentTemplate
property of the attached control to one which just contains the loading indicator; when it’s set to false
, it restores the original value to the attached control. Therefore, I’m going to use two dependency properties, a public one for the attached property and a private one for storing the current value of the ContentTemplate
before it is overwritten.
public static readonly DependencyProperty IsLoadingProperty =
DependencyProperty.RegisterAttached("IsLoading", typeof(bool),
typeof(AsyncIndicator), new PropertyMetadata(false, OnIsLoadingChanged));
private static readonly DependencyProperty OriginalTemplateProperty =
DependencyProperty.RegisterAttached("OriginalTemplate",
typeof(TemplateData), typeof(AsyncIndicator), new PropertyMetadata(null));
public static bool GetIsLoading(DependencyObject obj)
{
return (bool)obj.GetValue(IsLoadingProperty);
}
public static void SetIsLoading(DependencyObject obj, bool value)
{
obj.SetValue(IsLoadingProperty, value);
}
private static void OnIsLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
TemplateData data = BackupTemplateProperty(d);
if (data != null)
{
SetIndicator(d, data);
}
}
else
{
RestoreTemplateProperty(d);
}
}
private class TemplateData
{
public DependencyProperty Property { get; set; }
public object Template { get; set; }
}
The reason for the private
TemplateData
nested class is to allow for different properties of different controls to be backed up, replaced and restored (at the moment, I need ContentControl
and HubSection
, which don’t share the same ContentTemplate
property). Here’s the actual code which switches out the current template and replaces it with one that just contains the new control:
private static DependencyProperty GetTemplateProperty(object element)
{
if (element is ContentControl)
{
return ContentControl.ContentTemplateProperty;
}
else if (element is HubSection)
{
return HubSection.ContentTemplateProperty;
}
return null;
}
private static TemplateData BackupTemplateProperty(DependencyObject d)
{
TemplateData data = null;
DependencyProperty templateProperty = GetTemplateProperty(d);
if (templateProperty != null)
{
data = new TemplateData();
data.Property = templateProperty;
data.Template = d.GetValue(templateProperty);
}
d.SetValue(OriginalTemplateProperty, data);
return data;
}
private static void RestoreTemplateProperty(DependencyObject d)
{
var data = (TemplateData)d.GetValue(OriginalTemplateProperty);
if (data != null)
{
d.SetValue(data.Property, data.Template);
d.ClearValue(OriginalTemplateProperty);
}
}
private static void SetIndicator(DependencyObject d, TemplateData data)
{
const string IndicatorDataTemplate =
@"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:local='using:PurplePhobos.View.Controls'>
<local:AsyncIndicator />
</DataTemplate>";
d.SetValue(data.Property, XamlReader.Load(IndicatorDataTemplate));
}
To keep the code generalised, it tries to find a suitable dependency property based on the type of the control it is being attached to. If it finds a property, it then saves the current value of it to our private
attached property (this also works fine for static
resources, however, it won’t work for bindings, but there shouldn’t be much need for a binding of a DataTemplate
). A new DataTemplate
is created from a string
that contains the new control – note if you use this code and change the namespace of where the class is, be sure to update the string
as well to the new namespace! Finally, restoring is a lot easier, the control just needs to check if a template property has been backed up to the private
attached property and, if so, set it back (clearing the OriginalTemplateProperty
to reduce resources).
The full class can be found here – you’ll just need to style it somewhere in your app. Usage is something like this:
<HubSection ctrls:AsyncIndicator.IsLoading="{Binding Property.IsNotCompleted}"
ContentTemplate="{StaticResource ExampleTemplate}" />