A while ago one of my collegues from WPF Disciples showed me a video about a WPF app that Billy Hollis had put together. This app was written in VB .NET and had a very nice notes ListBox. There was supposed to be some source code published somewhere, but I couldn't find it. As such I tried the task myself and am pretty pleased with the results. This article respresents the fruits of my investigation/trials.
It is a re-usable Notes ListBox that can be applied to your own project with
not to much bother (I hope).
Here is what I will be covering in this article
Well it looks like this, by default, but as Josh Smith will probably point out, this may not suit peoples schemes/tastes. I say, thanks Josh, but they have the code, they can change these within the XAML. The problem with Mr Smith, is that he's just a lot brighter than the rest of us. I personally am quite happy about that, he is normally right.

The red bounding shape is the main focus of this article. This is the part I intended to be re-used, the rest of the UI is really just to demo the re-usable notes ListBox. Though there is 1 or 2 details that I will have to go through with you that you need to know, before you are able to re-use the attached notes ListBox in your own app. There is like 1 or 2 rules you need to adhere to.
What I wanted to create was a nice looking notes system, that could be re-used within someone elses app. I think I have managed to do this (ok you may have to change colors etc etc).
What the attached NotesListBox allows is as follows:
ObservableCollection<Note> Notes propertyNoteAdorner. This preserves your existing screen space
The NotesListBox is just a ListBox, but is I think it's a pretty funky one, that looks as follows:
There are a couple of things worth a mention. So I will stroll on and mention them. One of the things that I like, is how each item gets its own rotation. This is achieved using a ValueConverter (I use this for several index based binding conversions), that works with the current ListBoxItem index. Here is an example:
In XAML I have a Style for a ListBoxItem that looks like
<!-- ListBoxItem -->
<Style TargetType="ListBoxItem">
<Setter Property="Canvas.Left" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Canvas.Top">
<Setter.Value>
<Binding RelativeSource="{RelativeSource Self}"
Converter="{StaticResource myListIndexConverter}"
ConverterParameter="Top"/>
</Setter.Value>
</Setter>
<Setter Property="Canvas.ZIndex">
<Setter.Value>
<Binding RelativeSource="{RelativeSource Self}"
Converter="{StaticResource myListIndexConverter}"
ConverterParameter="ZIndex"/>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid x:Name="gridItem" Background="DarkGoldenrod" Width="100" Height="100">
<Border Background="LemonChiffon" Margin="2">
<ContentPresenter
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="0"/>
</Border>
<Grid.LayoutTransform>
<RotateTransform CenterX="0.5" CenterY="0.5"
Angle="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}, AncestorLevel=1},
Converter={StaticResource myListIndexConverter},
ConverterParameter='Rotate'}"/>
</Grid.LayoutTransform>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter TargetName="gridItem" Property="RenderTransform">
<Setter.Value>
<ScaleTransform CenterX="0.5" CenterY="0.5"
ScaleX="1.5" ScaleY="1.5"/>
</Setter.Value>
</Setter>
<Setter Property="Canvas.ZIndex" Value="99999"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Notice the use of the ValueConverter. Let's have a look at that shall we.
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Data;
using System.ComponentModel;
using System.Windows.Controls;
namespace NotesListBox
{
/// <summary>
/// Provides a OneWay converter that can provide a Top/ZIndex or Rotate
/// value for a give ListBoxItem, based on the source ListBoxItem index
/// </summary>
public class ListIndexConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
ListBoxItem item = (ListBoxItem)value;
ListBox listBox =
ItemsControl.ItemsControlFromItemContainer(item) as ListBox;
String paramValue = parameter.ToString();
Int32 index = listBox.ItemContainerGenerator.IndexFromContainer(item);
switch (paramValue)
{
case "Top":
return index * 80;
case "ZIndex":
return listBox.Items.Count - index;
case "Rotate":
return RotateAngle(index);
}
return value;
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException("can not convert back");
}
#endregion
#region Private Methods
private int RotateAngle(int index)
{
if (index == 0)
return -5;
if (index % 3 == 0)
return -15;
if (index % 2 == 0)
return 10;
if (index % 1 == 0)
return 6;
else
return 3;
}
#endregion
}
}
It can be seen that this one ValueConverter is used for a number of purposes associated with indexes within the associated ListBox.
For example is provides a Top/ZIndex and a Rotate binding value based on the current ListBoxItem index within the original ListBox
Other than that its all about Styles/Templates. SO I shall leave that as an
exercise for the reader. We will now go on to look at the NoteAdorner
object, and what it does for us. The NoteAdorner is an Adorner
that holds a single instance of a NotesListBox. For those of you
who have not heard of the AdornerLayer, you can think of it as
a special Layer that is on top of the current content.
MSDN states "An Adorner is a custom FrameworkElement that is bound to a UIElement.
Adorners are rendered in an AdornerLayer, which is a rendering surface that
is always on top of the adorned element or a collection of adorned elements.
Rendering of an adorner is independent from rendering of the UIElement that
the adorner is bound to. An adorner is typically positioned relative to the
element to which it is bound, using the standard 2-D coordinate origin located
at the upper-left of the adorned element.
So we can take advantage of this and use this layer to overlay items which don't
affect the layout of anything else. This is what the NoteAdorner
does. Its only job is to receive a ObservableCollection<Note> Notes
from the current object (the one you want to store notes with), which it passes
on to the hosted NotesListBox. The hosted NotesListBox
actually then takes ownership of the ObservableCollection<Note>
Notes that were passed to it, and will raise events when the user either
adds/removes/changes a note. This enables the end user the opportuntity to be
alerted when one of these actions happens. This will be explained a bit further
in the next section.
There is actually very little you need to be aware of when using this code, but you must follow the following 2 items, if you wish to use this NotesListBox in your own code.
Provide A Custom AdornerDecorator
As the NotesListBox is intended to work with the AdornerLayer,
you MUST use the custom AdornerDecorator (NotesAdornerDecorator)
that I have made. this article had orginally required the user to create an
in line XAML AdornerDecorator which wrapped their original content
and the user had to put code in their own application to manage the Adorner,
but Josh told me it would be better if you created a subclass of AdornerDecorator
where it managed itself and all the user had to do was put that in their XAML
and provide a property to it and wire up its events. So this is what I have
now done. The result is that the NotesListBox is very easy to use
in your own code now. You simply do the following:
Create a NotesAdornerDecorator somewhere in your main content
element (this is normally a Grid)
<Window x:Class="NotesListBoxTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:notes="clr-namespace:NotesListBox;assembly=NotesListBox"
xmlns:local="clr-namespace:NotesListBoxTest;assembly="
Title="Window1" Height="450" Width="650"
MinHeight="450" MinWidth="650"
WindowState="Maximized" WindowStartupLocation="CenterScreen">
<Grid>
<!-- Here is the actual content-->
<DockPanel LastChildFill="True" Background="#ff595959">
......
......
......
</DockPanel>
<!-- Here is my custom AdornerDecorator-->
<notes:NotesAdornerDecorator x:Name="notesAdornerDecorator" />
</Grid>
</Window>
This allows the NotesListBox to manage its own AdornerLayer.
All you have to do then is set the NotesAdornerDecorator.DisplayNotes
property and wire up the NotesAdornerDecorator events. This is
shown below
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using NotesListBox;
using System.Collections.ObjectModel;
namespace NotesListBoxTest
{
public partial class Window1 : Window
{
#region Ctor
public Window1()
{
InitializeComponent();
#region Wire up Routed Events
//Wire up the Note Added Event, which will come from the
//NotesListBoxControl on the AdornerLayer
EventManager.RegisterClassHandler(
typeof(NotesListBoxControl),
NotesListBoxControl.NoteAddedEvent,
new NoteEventHandler(
(s, ea) =>
{
Console.WriteLine(CreateNoteMessage(ea.Note));
}));
//Wire up the Note Removed Event, which will come from the
//NotesListBoxControl on the AdornerLayer
EventManager.RegisterClassHandler(
typeof(NotesListBoxControl),
NotesListBoxControl.NoteRemovedEvent,
new NoteEventHandler(
(s, ea) =>
{
Console.WriteLine(CreateNoteMessage(ea.Note));
}));
//Wire up the Note Changed Event, which will come from the
//NotesListBoxControl on the AdornerLayer
EventManager.RegisterClassHandler(
typeof(NotesListBoxControl),
NotesListBoxControl.NoteChangedEvent,
new NoteEventHandler(
(s, ea) =>
{
Console.WriteLine(CreateNoteMessage(ea.Note));
}));
#endregion
this.Loaded +=new RoutedEventHandler(Window1_Loaded);
}
#endregion
.....
.....
.....
private void lstPeople_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
notesAdornerDecorator.DisplayNotes = (lstPeople.SelectedItem as Person).Notes;
}
#endregion
}
}
Its that easy.
I think the bit of advise Josh Smith gave me had improved the re-usability a lot. So thanks for the idea Josh.
That's all I wanted to say this time, I hope it helps some of you. Could I just ask, if you liked this article please vote for it.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||