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:
Contents
As previously said, the subject is quite big. I will (try to) write this article in several parts:
- Part 1 - Introduction: Architecture and design
- Part 2 - User interface: Themes, dialogs, controls, external libraries
- Part 3 - Assembly: Schema, provider and parsing, threads and parallelism, controls
- Part 4 - Database: Schema, providers and plug-ins, controls
- Part 5 - Code generation: Template, engine, controls
- Part 6 - Performance: Injection, capture, and reports
- Part 7 - The project - Putting it all together
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:
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>
-->
<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>
-->
<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
- I can now override external assembly dictionary and Avalon is now in 3 color because I re-define his basic dictionary
- 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
- 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.
- Optimization: I do not remove existing and loaded dictionaries if they exist in the new skin or color.
UnLoadDictionaries
does not exist anymore.
- 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
- 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.
private void InitializeResource(List<ThemeElement> themeResourceList)
{
Tracer.Verbose("ThemeManager:InitializeResource", "START");
try
{
Application.Current.Resources.BeginInit();
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)
{
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)
LoadDictionaries(element);
Application.Current.Resources.EndInit();
}
[...]
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)
{
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 (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
.
<!---->
<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">
<!---->
<Rectangle Style="{StaticResource RectangleFrame}"/>
<!---->
<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"/>
<!---->
<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))]
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.
public void close_Click(object sender, RoutedEventArgs e)
{
if (HandleCloseAsHide)
this.Hide();
else
this.Close();
}
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 DependencyProperty
s : 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:
<!---->
<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:
- Selecting an item when a right mouse click occurs just before a context menu display
- Dummy node and
OnPopulateEvent
- this is not compatible with tree binding, only with manual filling
- 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:
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:
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
.
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:
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"))
{
}
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>
-->
<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 ResizingPanel
s and the log explorer at the bottom.
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">
-->
<ad:DockablePane>
-->
<UserControls:AssemblyExplorer x:Name="_DllExplorerDock" />
-->
<UserControls:DatabaseExplorer x:Name="_DBExplorerDock" />
-->
<UserControls:TemplateExplorer x:Name="_TemplateExplorerDock" />
</ad:DockablePane>
</ad:ResizingPanel>
-->
<ad:DocumentPane x:Name="_documentsHost">
-->
</ad:DocumentPane>
<ad:ResizingPanel Orientation="Vertical"
ad:ResizingPanel.ResizeWidth="200">
-->
<ad:DockablePane>
-->
<UserControls:ProjectExplorer x:Name="_ProjectExplorerDock" />
-->
<UserControls:PropertyExplorer x:Name="_PropertyExplorerDock" />
</ad:DockablePane>
</ad:ResizingPanel>
</ad:ResizingPanel>
<ad:ResizingPanel Orientation="Horizontal">
-->
<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:
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.
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:
protected override void OnPreviewMouseWheel(System.Windows.Input.MouseWheelEventArgs e)
{
base.OnPreviewMouseWheel(e);
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)
{
if (e.PropertyName == "ActiveDocument" && _dockingManager.ActiveDocument != null)
{
Tracer.Verbose("MainWindow.DockingManagerPropertyChanged",
"[{0}] '{1}' is the active document",
DateTime.Now.ToLongTimeString(),
_dockingManager.ActiveDocument.Title);
if (ActiveDocument is ZoomDocument)
{
this.MainStatusBar.ZoomChanged -=
new EventHandler<ZoomRoutedEventArgs>(
((ZoomDocument)ActiveDocument).OnZoomChanged);
((ZoomDocument)ActiveDocument).ZoomChanged -=
new ZoomDocument.ZoomChangedEventHandler(
this.MainStatusBar.OnZoomChanged);
}
ActiveDocument = (DocumentContent)_dockingManager.ActiveDocument;
if (this._dockingManager.ActiveDocument is ZoomDocument)
{
ZoomDocument zd =
(ZoomDocument)this._dockingManager.ActiveDocument;
this.MainStatusBar.CanZoom = true;
this.MainStatusBar.sliderZoom.Value = zd.Scale;
this.MainStatusBar.ZoomChanged +=
new EventHandler<ZoomRoutedEventArgs>(zd.OnZoomChanged);
((ZoomDocument)this._dockingManager.ActiveDocument).ZoomChanged +=
new ZoomDocument.ZoomChangedEventHandler(
this.MainStatusBar.OnZoomChanged);
}
else
{
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".
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);
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.
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
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");
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)
{
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:
- based on
DocumentContent
from Avalon
- support command like open, close, save and provide update in the recent file list
- support preview images (?)
- 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)) {
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 {
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
- Initial release - Version BETA 0.2
- Initial version that contains "nearly" everything, and is available for download on Part 1, or CodePlex as Release.
- Version BETA 0.3
- Update on skin/color management and the Database module
- Part 4 - Database module - is published