Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An Animated AlarmBar Custom Control in WPF

0.00/5 (No votes)
22 Jul 2008 1  
Published WPF resources discussing control customization focus almost exclusively on editing local copies of ControlTemplates, while implementing and interacting with an actual Custom Control library requires a DIFFERENT set of techniques and reference syntax to be used

Introduction

Many applications feature some kind of "message bar" to display error, warning, status, or other categories of messages to the user in an aesthetically pleasing and unobtrusive way. While the basic functionality is trivial to implement in WPF (just stretch a TextBlock across the top of your Window), in a production environment, you might need to create something which can be shared across projects, featuring subtle animation effects, supporting a simple XAML instantiation syntax...

The attached project implements an "AlarmBar" custom control supporting "Glass" and "Etched" visual styles and several animation options. Messages slowly fade-in, remain visible for several seconds, and then fade away (in a real-world application, supporting details would be written out separately, to a log of some kind).

NoAlarm.png

ErrorAlarm.png

WarningAlarm.png

Adding an AlarmBar, and customizing the bar's appearance and behavior, requires just a few lines of XAML, and you can define as many Alarm categories, with associated custom color schemes, as your application needs.  Trying to add a Button, or other invalid child, to the collection, will trigger an immediate (design time) error notification.

ClientXaml.png

To display an Alarm, you simply specify an Alarm id and message text:

alarmBar.Display( “Error”, “Containment breach imminent. Run for your lives!” )

WPF Custom Control Project Structure

Published WPF resources discussing control customization focus almost exclusively on editing local copies of ControlTemplates, while implementing and interacting with an actual Custom Control library requires a DIFFERENT set of techniques and reference syntax to be used. If this is your first exposure to WPF Custom Controls, you will probably have a much easier time understanding the project code if you create an empty WPF client project and walk through the following steps. Note that a generic Custom Control will be generated in Themes/generic.xaml, and you must NOT rename or move that file.

  • Use Add\New Project\WPF Custom Control Library to generate a default control and template.

  • Add a reference from your client project to the control project, and rebuild your solution.

  • Insert a line at the top of your client window XAML file to import the control namespace:

    xmlns:custom="clr-namespace:ControlProjectName;
    		assembly=ControlProjectAssemblyName" 
  • Add a control instance to your WPF window, set its background color, and run the application:

    <custom:CustomControl1 Background="Red" Height="20" VerticalAlignment="Top" /> 

Validating DESIGN-TIME XAML Entries

To display a message, the AlarmBar references information associated with one of the Alarm objects in its collection. The first decision you face in building a control like this is in choosing a base class to extend, in order to support adding Alarm objects (and ONLY Alarm objects) to your control via XAML. Deriving from ItemsControl would enable adding collection members through mark-up, but you would not be able to validate additions at design-time (You could catch errors at RUN time by manually iterating over members and checking their type, but that’s not a very satisfying work-around). By deriving from ContentControl, and specifying Alarm to be the supported collection type, we cause an error to be generated automatically, at DESIGN-TIME, if the user tries to add an invalid child to our collection. Note that, although an AlarmBar contains a list of Alarms, we would probably not want to use a ListBox as our base class, since we are never actually displaying a list (technically, we display properties associated with the single "active" list item).

AlarmBarContentProperty.png

Preferring XAML to Procedural Code

There is very little actual code involved in implementing our control. We simply define templates and styles containing display elements bound to a handful of DependencyProperties. The procedural code involved in displaying an Alarm consists of setting the “ActiveAlarm” property to the collection member whose id matches that contained in the display request, setting that Alarm object’s message text, and launching Storyboard animations.

The critical components that make up an AlarmBar’s default template are:

  • A TextBlock, reflecting current Alarm colors and the actual message text, clipped to conform to the control’s corner radius settings.
  • An optional secondary animation, such as an animated "pulse", which will run alongside the “fade-in/out” display sequence.
  • An optional static overlay (currently just used to create the “Glass” effect).

MainTemplate.png

The appearance and behavior of the secondary animation and static overlay elements are defined in separate styles and templates, which are “inserted” into the AlarmBar, based on “VisualStyle” and “AnimationStyleenum values (which are translated internally as described in the next section).

Hiding ComponentResourceKeys From the Client

There are two ways (ignoring reflection, etc. techniques) for client code to reference WPF logical resources located in an external assembly. In the case of a simple resource DLL, we can specify the path to the remote XAML file...

<ResourceDictionary Source="/SomeAssembly;component/SomeResourceDictionary.xaml" />

... and reference its resources using the same syntax we use locally.

MyProperty="{StaticResource SomeResourceIdString}"

Custom Controls often rely on resources identified in the context of a specific CLASS, using ComponentResourceKeys, which must be referenced from the client using one of the following syntax variations:

MyProperty="{DynamicResource {ComponentResourceKey TypeInTargetAssembly=
		{x:Type custom:SomeClass}, ResourceId= SomeResourceIdString }}" 
MyProperty="{DynamicResource {ComponentResourceKey {x:Type custom:SomeClass}, 
		SomeResourceIdString }}"
MyProperty="{StaticResource {x:Static custom:SomeClass.PropertyToExposeSomeResource}}"

This requires clients to have knowledge of specific ComponentResourceKey strings defined inside the control library, which may not always be desirable. Our control avoids this by exposing properties backed by Intellisense-friendly enum values to the client, and translating those enums internally to ComponentResourceKeys, before setting the underlying DependencyProperties. To eliminate an extra step, we use identical strings to define the enum value and its associated ComponentResourceKey.

Miscellaneous

XAML File Organization

An actual Custom Control Library will eventually contain many different controls, and dumping all of their associated mark-up into a single generic.xaml file will result in an unmaintainable mess. In our project, generic.xaml pulls information from a secondary XAML file (a known WPF bug currently requires specifying the full path when referencing sibling XAML files inside the Themes folder). We also extract color definitions, even those used in only one place, to a separate file. Colors tend to change frequently during development, and it is EXTREMELY tedious to try to pick out color values by scanning a growing body of non-trivial mark-up.

Animation Storyboards

Storyboards are “frozen” the instant they start running, causing attempts to bind the “To” or “From” properties of an animation to a dynamically-updated value, to fail, which is why we are forced to use the static SystemParameters.FullPrimaryScreenWidth value as the TranslateTransform destination in our “Blip” animation. Another option might be to explicitly reload the animation in reaction to window sizing events.

The expected architectural approach to launching an animation would be for the AlarmBar to raise an "AlarmChanged" RoutedEvent, handled in the template via an EventTrigger. While that works well when running a SINGLE animation, I was unable to get it to work in our situation, where we sometimes have two animations that must run in parallel in the context of two different Templates.  

Other Projects by Andy L.

History

  • 22nd July, 2008: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here