Introduction to Annotations.
What are WPF Annotations?
In the real world we can markup documents by highlighting content or attaching little notes. WPF provides this capability for content that is being viewed in one of the following built in text controls:
- FlowDocumentPageViewer
- FlowDocumentScrollViewer
- FlowDocumentReader
- DocumentViewer
How do I add Annotations to my Application?
If you are using one of the previous mentioned controls then you can quickly and easily enable annotations in your app.
For this example we’ll use a FlowDocumentPageViewer because we can define a FlowDocument all in XAML. Start with a window that looks like this:
<Window
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF Annotations Example"
>
<Grid>
<!-- Viewer and simple content -->
<FlowDocumentPageViewer Name="Viewer" Grid.Row="1">
<FlowDocument>
<Paragraph>
This is a simple example of a flow document and how you can add annotations to one.
</Paragraph>
<Paragraph>
Later we'll get more complicated content...
</Paragraph>
</FlowDocument>
</FlowDocumentPageViewer>
</Grid>
</Window>
Right now we don’t have any code, just a FlowDocumentPageViewer with a very simple document. The steps that we will have to go through to add annotations are:
- Create a UI for creating and deleting StickyNote and Highlight annotations.
- Write the code behind to enable and disable the AnnotationService
(1) WPF Annotations framework provides all the apis for creating, deleting, manipulating annotations, however, it is up to the specific application developer to create/provide a UI for invoking these commands/apis. The simplest way to construct your annotation UI is to use the Commands provided on the AnnotationService class. First we’ll need a namespace reference to the Annotation namespace to the root element of our Window:
xmlns:annot="clr-namespace:System.Windows.Annotations;assembly=PresentationFramework"
Now lets add a ToolBar to our app that contains some buttons that will invoke commands for creating and deleting StickyNotes (no code required):
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ToolBarTray Grid.Row="0">
<!-- Annotations Toolbar -->
<ToolBar>
<!-- StickyNote Operations -->
<GroupBox Header="Notes">
<StackPanel Orientation="Horizontal">
<Button
Content="Text"
Width="30"
Command="annot:AnnotationService.CreateTextStickyNoteCommand" />
<Button
Content="Ink"
Width="30"
Command="annot:AnnotationService.CreateInkStickyNoteCommand" />
<Button
Content="Delete"
Width="50"
Command="annot:AnnotationService.DeleteStickyNotesCommand" />
</StackPanel>
</GroupBox>
</ToolBar>
</ToolBarTray>
Build and run your app, you should see the toolbar at the top of the window. All of the buttons will be disabled. Now lets write the code to enable them.
(2) Now its time to write the code to actually turn annotations on in the application. Add the following event handlers to you window:
<Window
...
x:Class="MyNamespace.Window1"
Loaded="OnLoaded"
Unloaded="OnUnloaded">
Now add the code:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
// Turn Annotations On.
protected void OnLoaded(object sender, RoutedEventArgs e)
{
// Make sure that an AnnotationService isn’t already enabled.
AnnotationService service = AnnotationService.GetService(Viewer);
if (service == null)
{
// (a) Create a Stream for the annotations to be stored in.
AnnotationStream =
new FileStream("annotations.xml", FileMode.OpenOrCreate);
// (b) Create an AnnotationService on our
// FlowDocumentPageViewer.
service = new AnnotationService(Viewer);
// (c) Create an AnnotationStore and give it the stream we
// created. (Autoflush == false)
AnnotationStore store = new XmlStreamStore(AnnotationStream);
// (d) "Turn on annotations". Annotations will be persisted in
// the stream created at (a).
service.Enable(store);
}
}
// Turn Annotations off.
protected void OnUnloaded(object sender, RoutedEventArgs e)
{
// (a) Check that an AnnotationService actually
// existed and was Enabled.
AnnotationService service =
AnnotationService.GetService(Viewer);
if (service != null && service.IsEnabled)
{
// (b) Flush changes to annotations to our stream.
service.Store.Flush();
// (c) Turn off annotations.
service.Disable();
// (d) Close our stream.
AnnotationStream.Close();
}
}
// The stream that we will store annotations in.
Stream AnnotationStream;
}
Now build and run the app. When you make a selection within the FlowDocumentPageViewer the buttons in the ToolBar we created will automatically become enabled. Clicking “Add Text” or “Add Ink” will cause a StickyNote to be created within the document. Annotations will be stored in the file “annotations.xml” in your CWD. When you restart the app, your annotations will re-appear.
Note: the automatic enabling/disabling of the buttons will only occur if they are inside a ToolBar. This is because the ToolBar does some "magic" to dynamic change the CommandTarget of its children based on the focus of the app. If you aren't using a ToolBar then you will have to explicitly set the CommandTarget of each button so that they point to the viewer, e.g. CommandTarget="Viewer".
Lets look a little more closely at the code. In OnLoaded, we first initialize a Stream in which to load and save annotations from. It is important to understand that while the annotation framework will handle serializing the contents of the stream it is the appliation's responsibility to manage the Stream. That means that the application's responsibility to open, close, ensure permissions, and provide any security on the stream. It is also important to mention that the application should provide a different stream for each document that you are annotating. I'll provide some specific examples in future posts, but get in your head now that you should never use an annotations stream created on DocumentA when viewing/annotating DocumentB.
Now that we have our stream we'll create an AnnotationService for the viewer that we want to enable annotations on. AnnotationService is where all the action happens, note that there is one service per viewer, and one AnnotationStore per document. An AnnotationStore handles persisting the annotation information to disk in some way. The only built in store is the XmlStreamStore which, as you might expect, reads and writes from a Stream. The final step is to Enable the AnnotationService for our particular stream.
In UnLoaded event handler, we must explicitly Flush changes that we made to the Stream. Then Disable the AnnotationService. Finally, since it is our responsibility as the Application to manage the Stream, we have to close our Stream.
And thats it. Simple as Pie! We can create and delete StickyNote annotations on a FlowDocument. Since all the code is written for enabling annotations, adding highlights is as easy writing the following xaml to our window:
Add a resources section to our grid to define a style for our Highlight buttons:
<Grid.Resources>
<Style
x:Key="HighlightColorButtonStyle"
TargetType="{x:Type Button}" >
<Setter Property="Width" Value="20" />
</Style>
</Grid.Resources>
Add this GroupBox to our ToolBar:
<!-- Highlight Operations -->
<GroupBox Header="Highlight">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal">
<Button Background="Yellow" Style="{StaticResource HighlightColorButtonStyle}" Command="annot:AnnotationService.CreateHighlightCommand" CommandParameter="{x:Static Brushes.Yellow}" />
<Button Background="Green" Style="{StaticResource HighlightColorButtonStyle}" Command="annot:AnnotationService.CreateHighlightCommand" CommandParameter="{x:Static Brushes.Green}" />
<Button Background="Red" Style="{StaticResource HighlightColorButtonStyle}" Command="annot:AnnotationService.CreateHighlightCommand" CommandParameter="{x:Static Brushes.Red}" />
<Button Background="Blue" Style="{StaticResource HighlightColorButtonStyle}" Command="annot:AnnotationService.CreateHighlightCommand" CommandParameter="{x:Static Brushes.Blue}" />
<Button Background="Violet" Style="{StaticResource HighlightColorButtonStyle}" Command="annot:AnnotationService.CreateHighlightCommand" CommandParameter="{x:Static Brushes.Violet}" />
</StackPanel>
<Button Content="Clear" Width="50" Command="annot:AnnotationService.ClearHighlightsCommand" />
</StackPanel>
</GroupBox>
There you have it, with a small amount of code and some simple XAML for defining your UI you can now add Highlights and StickyNotes to all your FlowDocuments. The code and UI works exactly the same for the other viewers I mentioned before.
Note that in Feb CTP there are known issues using annotations in FlowDocumentScrollViewer and FlowDocumentReader so for this release it is probably best to stick with using FlowDocumentPageViewer for annotating FlowDocuments and DocumentViewer for annotating FixedDocuments..
This posting is provided "AS IS" with no warranties, and confers no rights.
Comments
- Anonymous
March 10, 2006
Annotations are very, very cool. I think the support for documents in WPF rocks, and annotations is a... - Anonymous
February 18, 2011
The comment has been removed