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

Reflection Studio - Part 2 - User Interface: Themes, Dialogs, Controls, External Libraries

0.00/5 (No votes)
22 Sep 2010 1  
Reflection Studio is a developer application for assembly, database, performance, and code generation, written in C# under WPF 4.0.

Introduction

This is the second part of my article about Reflection Studio. In this chapter, I describe generic user interface related components: writing themes with skins and colors, defining dialog templates, user controls, and using external assemblies like AvalonDock and Fluent to construct the main interface. Specific controls like assembly treeview will be discussed in the related article parts.

Reflection Studio is hosted at http://reflectionstudio.codeplex.com/. It is completely written in C# under the .NET/WPF platform. I recently moved to Visual Studio 2010 and NET 4. Please, have a look at the CodePlex project because it is too big to describe everything in detail. Here is a screenshot of the application:

ReflectionStudio

Contents

As previously said, the subject is quite big. I will (try to) write this article in several parts:

Part 2 - User Interface

All the generic user interface controls are in the Reflection.Studio.Controls assembly. The class schema below exposes the main classes and we will try to explore them in the following sections.

2.1 Themes: Skins and Colors

What disturbs me when starting WPF and watching a lot of samples, is that resource dictionaries that define themes are always appearance and color mixed. It is generally not possible to change the color of an "aero" style because everything is "hard coded" in the resource dictionary based on a blue glassy color. Even more, if I decide to use an external library, I am dependant on what type of style (skin + colors) was defined inside it. For the skin, why not? If it's well implemented, I can decide to override it with my own. But the colors? What to do if I want to support silver, blue, and black, but the library has no blue? Rewrite all the skins just for the color?

After seeing Fluent code, I found that way more flexible and logical. The solution is to define the basic or default color resources that must be used by the skin (templates). Color.xaml is the default, and the template uses DynamicResource. Add a Color.Blue.xaml, then your skin can have a different color.

For the external assemblies, I have actually no easy solution; that's why they are always used with the default skins, no matter the color... the result is not very nice!

The Helpers namespace contains two helpers for themes management that suits two different needs, and are described below.

2.1.1 - Fixed Skin and Colors in Themes

ThemeHelper can discover your embedded application themes and load them if you define a dictionary like below (in the main program), but take care of the assembly chaining and the performance issues. That was the previous Reflection Studio method.

  • <Resources>
    • <Themes>
      • <Black>
        • All dictionaries for the themes that must be included in the black.xaml file below
      • <Blue>
        • All dictionaries for the themes that must be included in the blue.xaml file below
      • Black.xaml
      • Blue.xaml

Here is a code example for using it. Fill workspace themes collection to be displayed in the user interface:

//load workspace values
WorkspaceService.Instance.Themes = ThemeHelper.DiscoverThemes();

Answer to a menu item click (containing the theme color as a string), and load the theme:

private void ThemeMenuItem_Click(object sender, RoutedEventArgs e)
{
    WorkspaceService.Instance.Entity.CurrentTheme =
               (string)((System.Windows.Controls.MenuItem)sender).Header;
    ThemeHelper.LoadTheme(WorkspaceService.Instance.Entity.CurrentTheme);
}

2.1.2 - Flexible Skins and Colors for Theme Composing

ThemeManager will load the skin and color dictionaries based on a configuration like below. A ThemeElement class is defined to serialize/deserialize the configuration file, and is used to apply the resource dictionary into the application. It helps to define colors and skin with the associated ResourceDictionary.

<ThemeElementCollection>
	<!--COLORS-->
	<ThemeElement Group="Colors" Name="Black" IsDefault="true" IsSelected="false" 
	Image="/ReflectionStudio;component/Resources/Images/32x32/color.png">
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Colors/Colors.Black.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/Common.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/Colors/ColorsBlack.xaml
		</Dictionary>
	</ThemeElement>
	<ThemeElement Group="Colors" Name="Blue" IsDefault="false" IsSelected="false" 
		Image="/ReflectionStudio;component/Resources/Images/32x32/color.png">
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Colors/Colors.Blue.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/Common.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/
			Colors/ColorsBlue.xaml</Dictionary>
	</ThemeElement>
	<ThemeElement Group="Colors" Name="Silver" IsDefault="false" IsSelected="false" 
		Image="/ReflectionStudio;component/Resources/Images/32x32/color.png">
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Colors/Colors.Silver.xaml</Dictionary>
		<Dictionary>/Fluent;component/Themes/Office2010/Common.xaml
			</Dictionary>
		<Dictionary>/Fluent;
			component/Themes/Office2010/Colors/ColorsSilver.xaml
		</Dictionary>
	</ThemeElement>
	<!--SKINS-->
	<ThemeElement Group="Skins" Name="Glossy" IsDefault="true" IsSelected="false" 
		Image="/ReflectionStudio;component/Resources/Images/32x32/skin.png">
		<Dictionary>/AvalonDock;component/themes/generic.xaml</Dictionary>
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Skins/Glossy.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
			Resources/Dictionnaries/AvalonDock.Glossy.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/Resources/
			AssemblyDesignerItem.xaml</Dictionary>
	</ThemeElement>
	<ThemeElement Group="Skins" Name="Blend" IsDefault="false" IsSelected="false" 
	Image="/ReflectionStudio;component/Resources/Images/32x32/skin_blend.png">
		<Dictionary>/AvalonDock;component/themes/generic.xaml</Dictionary>
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Skins/Blend.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
		Resources/Dictionnaries/AvalonDock.Expression.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
			Resources/AssemblyDesignerItem.xaml</Dictionary>
	</ThemeElement>
	<ThemeElement Group="Skins" Name="Visual Studio" IsDefault="false" 
		IsSelected="false" Image="/ReflectionStudio;component/
			Resources/Images/32x32/skin_studio.png">
		<Dictionary>/AvalonDock;component/themes/generic.xaml</Dictionary>
		<Dictionary>/ReflectionStudio.Controls;component/
			Resources/Skins/VisualStudio.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
			Resources/Dictionnaries/AvalonDock.Studio.xaml</Dictionary>
		<Dictionary>/ReflectionStudio;component/
			Resources/AssemblyDesignerItem.xaml</Dictionary>
	</ThemeElement>
</ThemeElementCollection>

To apply a theme resource, use the function LoadThemeResource below that removes the old resources, adds the new ones, and changes the configuration IsSelected flag so it can be saved/restored later.

Updates

  1. I can now override external assembly dictionary and Avalon is now in 3 color because I re-define his basic dictionary
  2. Changing the resource dictionary is now between a Application.Current.Resources.BeginInit(); and Application.Current.Resources.EndInit(); calls to be sure there is no live update on the user interface as we are changing templates
  3. I set up a specific function when starting - because resource contained in App.xaml can be different from the configured dictionary (when making design) - so I remove everything that does not exist in the initial definition.
  4. Optimization: I do not remove existing and loaded dictionaries if they exist in the new skin or color. UnLoadDictionaries does not exist anymore.
  5. Side Effect: Some controls like explorers were using the ApplyTemplate override to fill them up. This has been modified because each theme change is calling this method and I got multiple items in the tree for example
  6. I also add a debug functions to trace in a recursive manner all the dictionaries that are loaded in the application

Here under is the InitializeResource function that will be called by the Load method and the new LoadDictionaries that now takes two parameters with old and new resource definition.

/// <summary>
/// Load the specified ThemeElement list and remove old ones
/// </summary>
/// <param name="themeResourceList"></param>
private void InitializeResource(List<ThemeElement> themeResourceList)
{
	Tracer.Verbose("ThemeManager:InitializeResource", "START");

	//TraceDictionnaries();

	try
	{
		Application.Current.Resources.BeginInit();

		//remove old ones
		List<ResourceDictionary> olds = new List<ResourceDictionary>();
		List<string> newsItems = new List<string>();

		foreach (ThemeElement element in themeResourceList)
			newsItems.AddRange(element.Dictionaries);

		foreach (string dico in newsItems)
		{
			//in existing dictionary, be sure to remove old ones, 
			//in particular the on coming from xaml like App.xaml
			foreach (ResourceDictionary dictionnary in 
			Application.Current.Resources.MergedDictionaries)
			{
				if (newsItems.Find(p => p == 
				dictionnary.Source.OriginalString) == null)
					olds.Add(dictionnary);
			}
		}

		foreach (ResourceDictionary dictionnary in olds)
			Application.Current.Resources.MergedDictionaries.Remove
				(dictionnary);

		foreach (ThemeElement element in themeResourceList)
		//add new ones
			LoadDictionaries(element);

		Application.Current.Resources.EndInit();
	}
	[...]

/// <summary>
/// Load the specified ThemeElement in the application and set it as IsSelected
/// </summary>
/// <param name="themeResource"></param>
private void LoadDictionaries
	(ThemeElement oldThemeResource, ThemeElement newThemeResource)
{
	Tracer.Verbose("ThemeManager:LoadDictionaries", "START");
	try
	{
		try
		{
			foreach (string dictionnary in oldThemeResource.Dictionaries)
			{
				ResourceDictionary dic = 
				Application.Current.Resources.MergedDictionaries.
				FirstOrDefault(p => p.Source.OriginalString == 
				dictionnary);
				if (dic != null)
				{
					//does not exist in new ones
					if (newThemeResource.Dictionaries.Where
					(p => p == dic.Source.OriginalString).
						Count() == 0)
						Application.Current.Resources.
						MergedDictionaries.Remove(dic);
				}
			}
			oldThemeResource.IsSelected = false;
		}
		catch (Exception all)
		{
			Tracer.Error("ThemeManager.LoadDictionaries", all);
		}

		foreach (string dictionnary in newThemeResource.Dictionaries)
		{
			//if does not exist in application
			if (Application.Current.Resources.MergedDictionaries.
			Where(p => p.Source.OriginalString == 
				dictionnary).Count() == 0)
			{
				Uri Source = new Uri(dictionnary, UriKind.Relative);
				ResourceDictionary dico = 
				(ResourceDictionary)Application.LoadComponent(Source);
				dico.Source = Source;
				Application.Current.Resources.
					MergedDictionaries.Add(dico);
			}
		}
		newThemeResource.IsSelected = true;
	}
	catch (Exception all)
	{
		Tracer.Error("ThemeManager.LoadDictionaries", all);
	}
	Tracer.Verbose("ThemeManager:LoadDictionaries", "END");
}

You will then obtain a collection of resources that can be applied independently in the same group "color" or "skin". In Reflection Studio, I bind the whole collection with two galleries and a filter like below:

<Fluent:InRibbonGallery x:Name="inRibbonGallery_Color"
    ItemsSource ="{Binding Themes}"
    ItemTemplate="{StaticResource ColorDataItemTemplate}"
    Text="Colors" GroupBy="Group"
    ResizeMode="Both" MaxItemsInRow="3"
    MinItemsInRow="1" ItemWidth="40"
    ItemHeight="55" ItemsInRow="3"
    SelectionChanged="inRibbonGallery_Color_SelectionChanged">
      <Fluent:InRibbonGallery.Filters>
        <Fluent:GalleryGroupFilter Title="All" Groups="Colors" />
      </Fluent:InRibbonGallery.Filters>
</Fluent:InRibbonGallery>

Skins and colors are then displayed in the theme gallery on the main ribbon tab, as illustrated in the following image:

Below is an example of the "glossy" skin with the three colors. As you can't see, we need help for a better design...

2.2 Dialogs: Standard, Headered, MessageBox

I defined some templates and an associated class so that the dialogs are more compliant with the look and feel of the main Office Window style. This means rounded borders with shadow, systems buttons...

2.2.1 - WindowBase

As the assembly also contains an OfficeWindow that was previously used (in place of the Fluent window), there is a common window class to manage the sizing of it through a gripper. The gripper is not shown if the window has no ResizeMode.CanResizeWithGrip style.

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    if (!DesignerProperties.GetIsInDesignMode(this))
    {
        if (this.ResizeMode == System.Windows.ResizeMode.CanResizeWithGrip)
        {
            FrameworkElement resizeBottomRight =
                     (FrameworkElement)GetTemplateChild(ResizeGripPART);
            resizeBottomRight.MouseDown += OnResizeRectMouseDown;
            resizeBottomRight.MouseMove += OnResizeRectMouseMove;
            resizeBottomRight.MouseUp += OnResizeRectMouseUp;
        }
        else
        {
            FrameworkElement resizeBottomRight =
                     (FrameworkElement)GetTemplateChild(ResizeGripPART);
            resizeBottomRight.Visibility = System.Windows.Visibility.Hidden;
        }
    }
}

Depending on the style, we hook up the gripper to handle the resize logic, or hide it. A dialog cannot be sized by its border, so we will have to manage that in the derived classes. The OfficeWindow class and template are now out of scope, but look at the code, it is quite interesting.

2.2.2 - Dialog

DialogWindow is the base class for the HeaderedDialog and MessageBox that are generally used in Reflection Studio. Here is the startup dialog example that uses the DialogWindow as the base class and the about dialog as a HeaderedDialog sample:

The XAML style is quite simple, and defines a rounded border, a button, and gripper. Some properties are changed like AllowsTransparency, WindowStyle, Background, and ShowInTaskbar.

<!-- DialogWindow Style -->
<Style x:Key="{x:Type ucc:DialogWindow}" TargetType="{x:Type ucc:DialogWindow}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="AllowsTransparency" Value="True"/>
    <Setter Property="WindowStyle" Value="None"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="ShowInTaskbar" Value="False"/>
    <Setter Property="Template">
        <Setter.Value>
        <ControlTemplate TargetType="{x:Type ucc:DialogWindow}">

            <Grid Margin="10">
            <!--Windows Frame rectangle-->
            <Rectangle Style="{StaticResource RectangleFrame}"/>

            <!--PART_Close is the dialog close button-->
            <Button Style="{StaticResource closeButton}"
                x:Name="PART_Close" Height="11" Width="11"
                HorizontalAlignment="Right"
                Margin="0,9,11,0" VerticalAlignment="Top"
                ToolTip="Close" IsCancel="True"/>

            <!-- PART_ContentPresenter -->
            <ContentPresenter x:Name="PART_ContentPresenter"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Stretch"/>

            <ResizeGrip Grid.Column="0" HorizontalAlignment="Right"
                VerticalAlignment="Bottom"
                Width="17" Height="17"
                Focusable="False" Margin="0,0,8,8"
                x:Name="PART_ResizeGrip" Cursor="SizeNWSE"/>
            </Grid>
        </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The class DialogWindow defines a template PART_Close for the closing top right button, and we hook it up in the OnApplyTemplate.

[TemplatePart(Name = "PART_Close", Type = typeof(Button))]
/// <summary>
/// Find the template part to attach button click event handler
/// </summary>
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    if (!DesignerProperties.GetIsInDesignMode(this))
    {
        Button close = this.Template.FindName("PART_Close", this) as Button;

        if (close != null)
            close.Click += new RoutedEventHandler(close_Click);
    }
}

Then we handle the closing event and also the move.

/// <summary>
/// Handle the button click event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void close_Click(object sender, RoutedEventArgs e)
{
    if (HandleCloseAsHide)
        this.Hide();
    else
        this.Close();
}

/// <summary>
/// Handle the move event as for dialog background click
/// </summary>
/// <param name="e"></param>
protected override void OnMouseLeftButtonDown(
          System.Windows.Input.MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);
    this.DragMove();
}

To use it, define a new dialog and change the XAML and code-behind to have the DialogWindow as inherited from. Fill it, and you will get the template and default behaviour:

public partial class StartupDlg : DialogWindow
<controls:DialogWindow
    x:Class="ReflectionStudio.Components.Dialogs.Startup.StartupDlg"
    xmlns:controls="clr-namespace:ReflectionStudio.
                     Controls;assembly=ReflectionStudio.Controls"
...>

2.2.3 - HeaderedDialog

The HeaderedDialog inherits from the Dialog class and uses a HeaderControl in its template, so we get a sample like below:

The class defines two additional DependencyPropertys : DialogDescription and DialogImage of type string and ImageSource. Nothing more.

The HeaderedDialog has got a similar template to the DialogWindow and adds a DialogHeader which is template bound to the DependencyProperty of the class. To use it, define a new dialog, and change the XAML and code-behind to have the HeaderedDialog as inherited from:

<!--Header-->
<ucc:DialogHeader Grid.Row="0" x:Name="PART_Header"
    VerticalAlignment="Stretch" HasSeparator="Visible"
    Title="{TemplateBinding Property=Title}"
    Image="{TemplateBinding Property=DialogImage}"
    Description="{TemplateBinding Property=DialogDescription}" />

2.2.4 - MessageBox

The control assembly also defines a MessageBoxDlg class and template that match the existing one in WinForms. It inherits from HeaderedDialog.

The template is very simple and looks like below. Buttons and images will be configured at runtime:

The template is as below:

<ucc:HeaderedDialogWindow x:Class="ReflectionStudio.Controls.MessageBoxDlg"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ucc="clr-namespace:ReflectionStudio.Controls"
    Height="240" Width="600">
<Grid Margin="10,-20,10,10">
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="32" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="0.33*" />
        <ColumnDefinition Width="0.33*" />
        <ColumnDefinition Width="0.33*" />
    </Grid.ColumnDefinitions>
    <TextBlock Margin="10" Grid.ColumnSpan="3"
        x:Name="textBlockMessage" HorizontalAlignment="Stretch"
        TextWrapping="Wrap"
        FontSize="18" Text="tes de message pour voir la taille"/>
    <Button Grid.Row="1" x:Name="BtnLeft"
        IsDefault="True" Margin="26,0,29,0"
        Click="Btn_Click" />
    <Button IsDefault="True" Margin="30,0,31,0"
        Name="BtnMidle" Grid.Column="1"
        Grid.Row="1" Click="Btn_Click"></Button>
    <Button IsDefault="True" Margin="28,0,33,0"
        Name="BtnRight" Grid.Column="2"
        Grid.Row="1" Click="Btn_Click"></Button>
</Grid>
</ucc:HeaderedDialogWindow>

The MessageBoxDlg has two static methods for calling it:

public static MessageBoxResult Show(string message, string title)
{
    return MessageBoxDlg.Show(message, title,
           MessageBoxButton.OKCancel, MessageBoxImage.None);
}

public static MessageBoxResult Show(string message,
       string title, MessageBoxButton button, MessageBoxImage icon)
{
    MessageBoxDlg msgBox = new MessageBoxDlg();

    msgBox.Title = title;
    msgBox.textBlockMessage.Text = message;
    msgBox.DisplayButton(button);
    msgBox.DisplayIcon(icon);
    msgBox.ShowDialog();

    return msgBox.MessageBoxResult;
}

For using it, here is an example called with resources as title and message:

MessageBoxResult answer = MessageBoxDlg.Show(
        ReflectionStudio.Properties.Resources.MSG_PRJ_ASK_SAVE,
        ReflectionStudio.Properties.Resources.MSG_TITLE,
        MessageBoxButton.YesNoCancel,
        MessageBoxImage.Error);

2.3 - Controls

This namespace contains all the generic controls that are needed for the basic needs for Reflection Studio. Controls to come later are the PropertyGrid, WaitControl, Diagram.

2.3.1 - Buttons, Headers, ...

  • StandaloneHeader offers Title, Description, and Image properties, and can be used in panels, like a DialogHeader is used in DialogWindow.
  • FlatImageButton is used on the top of explorers. It only displays an image and has got an over effect.
  • ImageButton is a standard button with image. It has got complementary ImagePosition and Orientation properties.

2.3.2 - Images

AutoGreyableImage is a very useful image control that displays the grey version of an image when the IsEnabled property changes in the parent container. Here is an example of using it in a context menu item.

<MenuItem Header="Delete"
    DataContext="{Binding Path=PlacementTarget,
         RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
    Command="{x:Static local:DatabaseExplorer.DataSourceRefresh}"
    CommandParameter="{Binding Tag}"
    IsEnabled="{Binding Tag, Converter={StaticResource ObjectToBooleanConverter}}">
    <MenuItem.Icon>
        <controls:AutoGreyableImage
           Source="/ReflectionStudio;component/Resources/Images/
                   16x16/application/delete.png" Width="16"/>
    </MenuItem.Icon>
</MenuItem>

2.3.3 - Treeview

The TreeViewExtended class has got two main functionalities:

  1. Selecting an item when a right mouse click occurs just before a context menu display
  2. Dummy node and OnPopulateEvent - this is not compatible with tree binding, only with manual filling
  3. It is planned to have "new item" management, etc.

We manage the PreviewMouseRightButtonDown event on the tree to select the treeview item under the mouse, with the following code:

/// <summary>
/// Allow to select an item before the context menu pop's up
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void TreeViewExtended_PreviewMouseRightButtonDown(object sender,
             MouseButtonEventArgs e)
{
    TreeView control = sender as TreeView;

    IInputElement clickedItem = control.InputHitTest(e.GetPosition(control));

    while ((clickedItem != null) && !(clickedItem is TreeViewItem))
    {
        FrameworkElement frameworkkItem = (FrameworkElement)clickedItem;
        clickedItem = (IInputElement)(frameworkkItem.Parent ??
                         frameworkkItem.TemplatedParent);
    }

    if (clickedItem != null)
        ((TreeViewItem)clickedItem).IsSelected = true;
}

The treeview has a PopulateOnDemand dependency property and a special function to add items:

/// <summary>
/// Add a new treeview item to the specified parent.
/// Manage dummy node and PopulateOnDemand
/// </summary>
public TreeViewItem AddItem(TreeViewItem parent,
       string label, object tag, bool needDummy = true)
{
    TreeViewItem node = new TreeViewItem();
    node.Header = label;
    node.Tag = tag;

    if (PopulateOnDemand && needDummy)
    {
        node.Expanded += new RoutedEventHandler(node_Expanded);
        node.Items.Add(new TreeViewItemDummy());
    }

    if (parent != null)
        parent.Items.Add(node);
    else
        this.Items.Add(node);

    return node;
}

This function will (in case you set PopulateOnDemand to true - the needDummy parameter is true by default ) always add a TreeViewItemDummy element, and also an event handler to manage each item's expanded event (does not exist anymore on the tree control in WPF).

When an item is expanded, the treeview checks the item type against the TreeViewItemDummy type, and if necessary, removes it and sends a ItemNeedPopulateEvent.

/// <summary>
/// Internal management of the treeview item expansion.
/// remove the dummy node if needed and fire the ItemNeedPopulateEvent
/// event when the PopulateOnDemand property is set
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void node_Expanded(object sender, RoutedEventArgs e)
{
    TreeViewItem opened = (TreeViewItem)sender;

    if (opened.Items[0] is TreeViewItemDummy && PopulateOnDemand)
    {
        opened.Items.Clear();
        RaiseItemNeedPopulate(opened);
    }
}

To use it, first plug your handler on the OnItemNeedPopulate event handler :

this.treeViewDB.OnItemNeedPopulate +=
   new TreeViewExtended.ItemNeedPopulateEventHandler(treeViewDB_OnItemNeedPopulate);

Then add your tree items:

//server connection sample
TreeViewItem tn = this.treeViewDB.AddItem(null, source.Name, source);

In case you know that there are no children, set the needDummy parameter to false - the AddItem function will handle it.

...
    foreach (IndexSchema ts in ((TableSchema)parent.Tag).Indexes)
        this.treeViewDB.AddItem(openedItem, ts.Name, ts, false);
...

2.3.4 - Helpers

There are various helpers in the control assembly:

  • LongOperation: manages the cursor and starts/stops the progress bar.
  • using (new LongOperation(this, "Execute"))
    {
        //...very long operation or not threaded so ui must not respond...
    }
  • VisualHelper: allows to save a visual element as a BitmapImage.

2.4 - Converters

The basic converters are in the Controls assembly. You will find additional ones in the main program:

  • ReflectionStudio.Controls
    • BoolToVisibilityConverter
    • EnumToStringConverter: for simple string conversion when enum is human readable
  • ReflectionStudio
    • ScaleToPercentConverter: double value to percentage and reverse
    • ObjectToBooleanConverter: converts objects (null or not) to boolean for enabling menu items
    • NetTypeToImageConverter: for the assembly treeview to display type images
    • LogTypeToImageConverter: for the log toolbox to display error icon
    • FileInfoToImageConverter: for the template treeview to display folder or file images
    • DockStateToBooleanConverter: convert the Avalon DockState enum to a visibility boolean
    • DBTypeToImageConverter: for the database treeview to display object images

2.5 - Externals Libraries

2.5.1 - AvalonDock and Fluent

Integrating the AvalonDock and Fluent libraries from CodePlex starts with the MainWindow. Define the main skeleton of the window with these three rows: Ribbon, Content, and StatusBar.

<Fluent:RibbonWindow x:Class="ReflectionStudio.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Fluent="clr-namespace:Fluent;assembly=Fluent"
    xmlns:ad="clr-namespace:AvalonDock;assembly=AvalonDock"
    xmlns:cmd="clr-namespace:ReflectionStudio.Classes"
    xmlns:Controls=
      "clr-namespace:ReflectionStudio.Controls;
       assembly=ReflectionStudio.Controls"
    xmlns:UserControls="clr-namespace:ReflectionStudio.Components.UserControls"
    xmlns:converters="clr-namespace:ReflectionStudio.Components.Converters"
    ResizeMode="CanResizeWithGrip"
    Title="{Binding Title}" Height="600" Width="800"
    Loaded="OfficeWindow_Loaded"
    Closing="OfficeWindow_Closing" Drop="OfficeWindow_Drop"
    Icon="Resources\Images\16x16\ReflectionStudio.png">

<Fluent:RibbonWindow.Resources...

    <Grid Name="MainGrid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <!--RIBBON CONTROL-->
        <Fluent:Ribbon ...

        <!--CONTENT-->
        <ad:DockingManager ...

        <!--STATUS BAR-->
        <UserControls:StatusBar Grid.Row="2" x:Name="MainStatusBar" />

    </Grid>

</Fluent:RibbonWindow>

Very simple and my implementation of the Fluent control is based on the project documentation. More touchy is the layout for the AvalonDock content. I had some resizing trouble at the beginning and solved it with the following layout: a main vertical ResizingPanel containing two horizontal ResizingPanels and the log explorer at the bottom.

layout

Hopefully, the explorers are UserControls, so that the main XAML stays readable. Below is the content structure:

<ad:DockingManager Grid.Row="1" x:Name="_dockingManager"
         Loaded="_dockingManager_Loaded"
         Background="{DynamicResource DefaultBorderBrush}">
    <ad:ResizingPanel Orientation="Vertical">
        <ad:ResizingPanel Orientation="Horizontal">
            <ad:ResizingPanel Orientation="Vertical"
                     ad:ResizingPanel.ResizeWidth="200">
                <!--LEFT PART-->
                <ad:DockablePane>

                    <!--ASSEMBLY EXPLORER -->
                    <UserControls:AssemblyExplorer x:Name="_DllExplorerDock" />

                    <!--DATABASE EXPLORER -->
                    <UserControls:DatabaseExplorer x:Name="_DBExplorerDock" />

                    <!--DATABASE EXPLORER -->
                    <UserControls:TemplateExplorer x:Name="_TemplateExplorerDock" />

                </ad:DockablePane>

            </ad:ResizingPanel>

            <!--CENTER PART-->
            <ad:DocumentPane x:Name="_documentsHost">

                <!--here goes the documents-->

            </ad:DocumentPane>

            <ad:ResizingPanel Orientation="Vertical"
                       ad:ResizingPanel.ResizeWidth="200">
                <!--RIGHT PART-->
                <ad:DockablePane>

                    <!--PROJECT EXPLORER-->
                    <UserControls:ProjectExplorer x:Name="_ProjectExplorerDock" />

                    <!--PROPERTY EXPLORER-->
                    <UserControls:PropertyExplorer x:Name="_PropertyExplorerDock" />

                </ad:DockablePane>

            </ad:ResizingPanel>
        </ad:ResizingPanel>

    <ad:ResizingPanel Orientation="Horizontal">

        <!--LOGS EXPLORER-->
        <ad:DockablePane>
            <UserControls:EventLogExplorer x:Name="_LogExplorerDock" />
        </ad:DockablePane>
    </ad:ResizingPanel>

</ad:ResizingPanel>
</ad:DockingManager>

2.6 - Common Controls

I am describing in this chapter the classes and user controls in the application that will not fit in any other article part. We have the explorers that will be discussed later, the StatusBar and the documents:

explorers

2.6.1 - Documents and StatusBar

Everything in the Reflection Studio UI is based on Document and Explorers. Every explorer derives from DockableContent and is simple. Documents derive from ZoomDocument or directly from DocumentContent in AvalonDock.

That's the most interesting part: ZoomDocument holds a Scale property and a ScaleTransformer that you must associate with a content in the derived class.

//QueryDocument
SyntaxEditor.TextArea.LayoutTransform = base.ScaleTransformer;

As it manages the mouse wheel event in preview mode, we can update the scale, the transformer, and raise a zoom event:

/// <summary>
/// Handle CTRL WHEEL for zooming
/// </summary>
/// <param name="e"></param>
protected override void OnPreviewMouseWheel(System.Windows.Input.MouseWheelEventArgs e)
{
    base.OnPreviewMouseWheel(e);

    //zooming
    if (Keyboard.IsKeyDown(Key.LeftCtrl))
    {
        UpdateContent(e.Delta > 0);
        e.Handled = true;
    }
}

And the main part is shown below. Catch the active document by hooking the DockingManager's PropertyChanged event to get the active document so we can handle the zoom changed event in both directions. If needed (in case of ZoomDocument based classes), I remove the zoom handler on the previous active document, then I add it to the new one. Note that the StatusBar control has a CanZoom property to disable the zoom slider and that the document initially sets the scale value.

void DockingManagerPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    //manage the active document
    if (e.PropertyName == "ActiveDocument" && _dockingManager.ActiveDocument != null)
    {
        Tracer.Verbose("MainWindow.DockingManagerPropertyChanged",
                       "[{0}] '{1}' is the active document",
                       DateTime.Now.ToLongTimeString(),
                       _dockingManager.ActiveDocument.Title);

        //remove the previous doc from event handling
        if (ActiveDocument is ZoomDocument)
        {
            this.MainStatusBar.ZoomChanged -=
              new EventHandler<ZoomRoutedEventArgs>(
              ((ZoomDocument)ActiveDocument).OnZoomChanged);
            ((ZoomDocument)ActiveDocument).ZoomChanged -=
              new ZoomDocument.ZoomChangedEventHandler(
              this.MainStatusBar.OnZoomChanged);
        }

        // save the new active doc
        ActiveDocument = (DocumentContent)_dockingManager.ActiveDocument;

        //plug to zoom event handling if needed
        if (this._dockingManager.ActiveDocument is ZoomDocument)
        {
            ZoomDocument zd =
              (ZoomDocument)this._dockingManager.ActiveDocument;

            this.MainStatusBar.CanZoom = true;
            this.MainStatusBar.sliderZoom.Value = zd.Scale;

            //root status bar update to the document
            this.MainStatusBar.ZoomChanged +=
              new EventHandler<ZoomRoutedEventArgs>(zd.OnZoomChanged);

            //root doc event to the status bar
            ((ZoomDocument)this._dockingManager.ActiveDocument).ZoomChanged +=
              new ZoomDocument.ZoomChangedEventHandler(
              this.MainStatusBar.OnZoomChanged);
        }
        else
        {
            //disable the zoom slider
            this.MainStatusBar.CanZoom = false;
        }
....

2.6.2 - Generic Home and Help documents

The HelpDocument is simply templated to include an XPS viewer, and we set the DocumentViewer property with the following code in the loading function:

XpsDocument xpsHelp = new XpsDocument(System.IO.Path.Combine(
      PathHelper.ApplicationPath, ((DocumentDataContext)DataContext).FullName),
      System.IO.FileAccess.Read);
documentViewer1.Document = xpsHelp.GetFixedDocumentSequence();

See Part 1 for a screenshot. Note that the file to display is coming as a parameter from the command and is defined in the MainWindow.xaml.

The HomeDocument is a bit more complicated. As I would like it to be updatable from the internet, I had to include a default "Feed not available content", then add a resource that can be saved/loaded by default, and an update function to get the last version from the internet. At start, I create a BackgroundWorker to get the URL content, so the document displays the "Feed not available content".

/// <summary>
/// Load the XAML if not in design mode in a background thread
/// </summary>
public override void OnApplyTemplate()
{
    Tracer.Verbose("HomeDocument:OnApplyTemplate", "START");

    if (!DesignerProperties.GetIsInDesignMode(this))
    {
        try
        {
            string urlToRead = "http://i3.codeplex.com/Project/Download/" +
                   "FileDownload.aspx?ProjectName=ReflectionStudio&DownloadId=132959";
            string destFile = System.IO.Path.Combine(
                   PathHelper.ApplicationPath, "Home.xaml");

            BackgroundWorker webWorker = new BackgroundWorker();
            webWorker.WorkerReportsProgress = false;
            webWorker.WorkerSupportsCancellation = false;
            webWorker.DoWork += new DoWorkEventHandler(bw_DoWork);
            webWorker.RunWorkerCompleted +=
               new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);

            // Start the asynchronous operation.
            webWorker.RunWorkerAsync(new UrlSaveHelper(urlToRead, destFile));

        }
        catch (Exception all)
        {
            Tracer.Error("HomeDocument.OnApplyTemplate", all);
        }
    }

    Tracer.Verbose("HomeDocument:OnApplyTemplate", "END");
}

After that, if the worker succeeds, we try to load the XAML (wrong in the case of a proxy response) - that's why I save and load the resource file in the catch statement. Finally, I parse the XAML to associate HyperLink elements with the code-behind.

/// <summary>
/// After the thread complete, load the xaml home document
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // First, handle the case where an exception was thrown.
    if (e.Error != null)
    {
        ...
    }
    else
    {
        Tracer.Verbose("HomeDocument:bw_RunWorkerCompleted", "START");

        UrlSaveHelper hlp = (UrlSaveHelper)e.Result;

        string destFile =
          System.IO.Path.Combine(PathHelper.ApplicationPath, "Home.xaml");

        // Finally, handle the case where the operation succeeded.
        if (File.Exists(destFile))
        {
            try
            {
                FileStream fs = File.OpenRead(destFile);
                FlowDocViewer.Document = (FlowDocument)XamlReader.Load(fs);
                fs.Close();

                Tracer.Verbose("HomeDocument:bw_RunWorkerCompleted",
                               "Internet document loaded");
            }
            catch (Exception)
            {
                //load the config from resources
                using (Stream fs = 
		Application.ResourceAssembly.GetManifestResourceStream(
                             "ReflectionStudio.Resources.Embedded.Home.xaml"))
                {
                    if (fs == null)
                        throw new InvalidOperationException(
                              "Could not find embedded resource");

                    FlowDocViewer.Document = (FlowDocument)XamlReader.Load(fs);
                    fs.Close();

                    Tracer.Verbose("HomeDocument:bw_RunWorkerCompleted",
                                   "Local document loaded");
                }
            }

            ParseHyperlink(FlowDocViewer.Document);

            Tracer.Verbose("HomeDocument:bw_RunWorkerCompleted", "END");
        }
    }
}

An improvement is planned to develop a RSS feed control to get news from CodePlex or CodeProject that can be embedded in it.

2.6.3 - Document Factory

Over all these document user control types, I have created a DocumentFactory to help in document management. This class is a singleton that bridges us with the AvalonDockManager and all documents that follow the next rules:

  1. based on DocumentContent from Avalon
  2. support command like open, close, save and provide update in the recent file list
  3. support preview images (?)
  4. support a common data context based on his type and associated file

The factory has got some simple functions like Find, Open, Get... Below is an example of how to open the previously discussed documents Help and Home. I am not going further in the description of this module because it will change.

private void DisplayHomeDocument()
{
	DocumentFactory.Instance.OpenDocument
		(DocumentFactory.Instance.SupportedDocuments.Find
		(p => p.DocumentContentType == typeof(HomeDocument)),
		new DocumentDataContext() { FullName = "Home", Entity = null });
}

private void DisplayHelpDocument(string fileName)
{
	DocumentFactory.Instance.OpenDocument
		(DocumentFactory.Instance.SupportedDocuments.Find
		(p => p.DocumentContentType == typeof(HelpDocument)),
		new DocumentDataContext() { FullName = fileName, Entity = null });
}

or like below in the general "New Document" function, we create a document by passing the type or the file extension:

public void NewCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
	if (string.IsNullOrEmpty((string)e.Parameter)) //default
	{
		//display the dialog
		NewDocumentDlg Dlg = new NewDocumentDlg();
		Dlg.Owner = Application.Current.MainWindow;
		Dlg.DataContext = DocumentFactory.Instance.SupportedDocuments.Where
			( p => p.CanCreate == true ).ToList();

		if (Dlg.ShowDialog() == true)
			DocumentFactory.Instance.CreateDocument
				( Dlg.DocumentTypeSelected );
	}
	else //file type as parameter
	{
		DocumentFactory.Instance.CreateDocument
		(DocumentFactory.Instance.SupportedDocuments.Find
			(p => p.Extension == (string)e.Parameter));
	}
	e.Handled = true;
}

Conclusion / Feedback

See you in the next article of this series. Do not hesitate to give me feedback about it either on Codeplex or CodeProject. As the team is growing, I hope that we are getting faster and do not hesitate to join us!

Article / Software History

  1. Initial release - Version BETA 0.2
    • Initial version that contains "nearly" everything, and is available for download on Part 1, or CodePlex as Release.
  2. Version BETA 0.3
    • Update on skin/color management and the Database module
    • Part 4 - Database module - is published

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