Last time, I showed you how to get started building extensions for Expression Blend. Let's build a useful extension this time and go a bit deeper into Blend.
- Source of project => here
- Compiled DLL => here (extract into /extensions folder of Expression Blend)
The Extension
When working on large XAML files in Blend, it’s often hard to find a specific control in the "Objects and Timeline Pane”. An extension that searches the active document and presents all elements that satisfy the query would be helpful. When the user starts typing a search query, a search will be performed and the results are shown in the list. After the user selects an item in the results list, the control in the "Objects and Timeline Pane” will be selected. Below is a sketch of what it is going to look like.
The Solution
Create a new WPF User Control project as shown in the earlier tutorial in the Configuring the extension project section, but name it AdvancedSearch
this time. Delete the default UserControl1.Xaml to clear the solution (a new user control will be added later though, but adding a user control is easier than renaming one).
Create the main entry point of the addin by adding a new class to the solution and naming this AdvancedSearchPackage
. Add a reference to Microsoft.Expression.Extensibility
and to System.ComponentModel.Composition
. Implement the IPackage
interface and add the Export
attribute from the MEF to the definition. While you’re at it, add references to Microsoft.Expression.DesignSurface
, Microsoft.Expression.FrameWork
and Microsoft.Expression.Markup
. These will be used later.
The Load
method from the IPackage
interface is going to create a ViewModel
to bind to from the UI. Add another class to the solution and name this AdvancedSearchViewModel
. This class needs to implement the INotifyPropertyChanged
interface to enable notifications to the view. Add a constructor to the class that takes an IServices
interface as a parameter.
Create a new instance of the AdvancedSearchViewModel
in the load
method in the AdvanceSearchPackage
class. The AdvancedSearchPackage
class should look like this now:
using System.ComponentModel.Composition;
using Microsoft.Expression.Extensibility;
namespace AdvancedSearch
{
[Export(typeof(IPackage))]
public class AdvancedSearchPackage:IPackage
{
public void Load(IServices services)
{
new AdvancedSearchViewModel(services);
}
public void Unload()
{
}
}
}
Add a new UserControl
to the project and name this AdvancedSearchView
. The View
will be created by the ViewModel
, which will pass itself to the constructor of the view. Change the constructor of the View
to take a AdvancedSearchViewModel
object as a parameter. Add a private
field to store the ViewModel
and set this field in the constructor. Point the DataContext
of the view to the ViewModel
. The View
will look something like this now:
namespace AdvancedSearch
{
public partial class AdvancedSearchView:UserControl
{
private readonly AdvancedSearchViewModel _advancedSearchViewModel;
public AdvancedSearchView(AdvancedSearchViewModel advancedSearchViewModel)
{
_advancedSearchViewModel = advancedSearchViewModel;
InitializeComponent();
this.DataContext = _advancedSearchViewModel;
}
}
}
The View
is going to be created in the constructor of the ViewModel
and stored in a read only property.
public FrameworkElement View
{
get; private set;
}
public AdvancedSearchViewModel(IServices services)
{
_services = services;
View = new AdvancedSearchView(this);
}
The last thing the solution needs before we’ll wire things up is a new class, PossibleNode
. This class will be used later to store the search results. The solution should look like this now:
Adding UI to the UI
The extension should build and run now, although nothing is showing up in Blend yet. To enable the user to perform a search query, add a TextBox
and a ListBox
to the AdvancedSearchView.xaml file. I’ve set the rows of the grid too to make them look a little better. Add the TextChanged
event to the TextBox
and the SelectionChanged
event to the ListBox
, we’ll need those later on.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox TextChanged="SearchQueryTextChanged"
HorizontalAlignment="Stretch"
Margin="4"
Name="SearchQuery"
VerticalAlignment="Stretch" />
<ListBox SelectionChanged="SearchResultSelectionChanged"
HorizontalAlignment="Stretch"
Margin="4"
Name="SearchResult"
VerticalAlignment="Stretch"
Grid.Row="1" />
</Grid>
This will create a user interface like:
To make the View show up in Blend, it has to be registered with the WindowService
. The GetService<T>
method is used to get services from Blend, which are your entry points into Blend. When writing extensions, you will encounter this method very often. In this case, we’re asking for an IWindowService
interface. The IWindowService
interface serves events for changing windows and themes, is used for adding or removing resources and is used for registering and unregistering Palettes
. All panes in Blend are palettes and are registered through the RegisterPalette
method.
The first parameter passed to this method is a string
containing a unique ID for the palette. This ID can be used to get access to the palette later.
The second parameter is the View
.
The third parameter is a title for the pane. This title is shown when the pane is visible. It is also shown in the window menu of Blend.
The last parameter is a KeyBinding
. I have chosen Ctrl+Shift+F to call the Advanced Search pane. This value is also shown in the window menu of Blend.
services.GetService<IWindowService>().RegisterPalette(
"AdvancedSearch",
viewModel.View,
"Advanced Search",
new KeyBinding
{
Key = Key.F,
Modifiers = ModifierKeys.Control | ModifierKeys.Shift
}
);
You can compile and run now. After Blend starts, you can hit Ctrl+Shift+F or go the windows menu to call the advanced search extension.
Searching for Controls
The search has to be cleared on every change of the active document. The DocumentServices
fires an event every time a new document is opened, a document is closed or another document view is selected. Add the following line to the constructor of the ViewModel
to handle the ActiveDocumentChanged
event:
_services.GetService<IDocumentService>().ActiveDocumentChanged += ActiveDocumentChanged;
And implement the ActiveDocumentChanged
method:
private void ActiveDocumentChanged(object sender, DocumentChangedEventArgs e)
{
}
To get to the contents of the document, we first need to get access to the “Objects and Timeline” pane. This pane is registered in the PaletteRegistry
in the same way as this extension has registered itself. The palettes are accessible through an associative array. All you need to provide is the Identifier
of the palette you want. The Id
of the “Objects and Timeline” pane is “Designer_TimelinePane
”. I’ve included a list of the other default panes at the bottom of this article. Each palette has a Content
property which can be cast to the type of the pane.
var timelinePane = (TimelinePane)_services.GetService<IWindowService>()
.PaletteRegistry["Designer_TimelinePane"]
.Content;
Add a private
field to the top of the AdvancedSearchViewModel
class to store the active SceneViewModel
. The SceneViewModel
is needed to set the current selection and to get the little icons for the type of control.
private SceneViewModel _activeSceneViewModel;
When the active SceneViewModel
changes, the ActiveSceneViewModel
is stored in this field. The list of possible nodes is cleared and an PropertyChanged
event is fired for this list to notify the UI to clear the list. This will make the eventhandler
look like this:
private void ActiveDocumentChanged(object sender, DocumentChangedEventArgs e)
{
var timelinePane = (TimelinePane)_services.GetService<IWindowService>()
.PaletteRegistry["Designer_TimelinePane"].Content;
_activeSceneViewModel = timelinePane.ActiveSceneViewModel;
PossibleNodes = new List<PossibleNode>();
InvokePropertyChanged("PossibleNodes");
}
The PossibleNode
class is used to store information about the controls found by the search. It’s a dumb data class with only 3 properties, the name of the control, the SceneNode
and a brush used for the little icon. The SceneNode
is the base class for every possible object you can create in Blend, like Brushes
, Controls
, Annotations
, ResourceDictionaries
and VisualStates
. The entire PossibleNode
class looks like this:
using System.Windows.Media;
using Microsoft.Expression.DesignSurface.ViewModel;
namespace AdvancedSearch
{
public class PossibleNode
{
public string Name { get; set; }
public SceneNode SceneNode { get; set; }
public DrawingBrush IconBrush { get; set; }
}
}
Add these two methods to the AdvancedSearchViewModel
class:
public void Search(string searchText) { }
public void SelectElement(PossibleNode node){ }
Both these methods are going to be called from the view. The Search
method performs the search and updates the PossibleNodes
list. The controls in the active document can be accessed through TimeLineItemsManager
class. This class contains a read only collection of TimeLineItems
. By using a Linq query, the possible nodes are selected and placed in the PossibleNodes
list.
var timelineItemManager = new TimelineItemManager(_activeSceneViewModel);
PossibleNodes =
new List<PossibleNode>(
(from d in timelineItemManager.ItemList
where d.DisplayName.ToLowerInvariant().StartsWith( searchText.ToLowerInvariant())
select new PossibleNode()
{
IconBrush = d.IconBrush,
SceneNode = d.SceneNode,
Name = d.DisplayName
}).ToList()
);
InvokePropertyChanged(InternalConst.PossibleNodes);
The Select
method is pretty straight forward. It contains two lines.The first to clear the selection. Otherwise the selected element would be added to the current selection. The second line selects the nodes. It is given a new array with the node to be selected.
_activeSceneViewModel.ClearSelections();
_activeSceneViewModel.SelectNodes(new[] { node.SceneNode });
The last thing that needs to be done is to wire the whole thing to the View
. The two event handlers just call the Search
and SelectElement
methods on the ViewModel
.
private void SearchQueryTextChanged(object sender, TextChangedEventArgs e)
{
_advancedSearchViewModel.Search(SearchQuery.Text);
}
private void SearchResultSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(e.AddedItems.Count>0)
{
_advancedSearchViewModel.SelectElement(e.AddedItems[0] as PossibleNode);
}
}
The Listbox has to be bound to the PossibleNodes
list and a simple DataTemplate
is added to show the selection. The IconWithOverlay
control can be found in the Microsoft.Expression.DesignSurface.UserInterface.Timeline.UI
namespace in the Microsoft.Expression.DesignSurface
assembly. The ListBox
should look something like:
<ListBox SelectionChanged="SearchResultSelectionChanged"
HorizontalAlignment="Stretch" Margin="4"
Name="SearchResult" VerticalAlignment="Stretch" Grid.Row="1"
ItemsSource="{Binding PossibleNodes}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<tlui:IconWithOverlay Margin="2,0,10,0"
Width="12" Height="12"
SourceBrush="{Binding Path=IconBrush, Mode=OneWay}"
/>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Compile and run. Inside Blend, the extension could look something like below:
What’s Next
When you’ve got the extension running, try placing breakpoints in the code and see what else is in there. There’s a lot to explore and build extension on. I personally would love an extension to search for resources.
Last but not least, you can download the source of project here. If you have any questions, let me know.
If you just want to use this extension, you can download the compiled DLL here. Just extract the . zip into the /extensions folder of Expression Blend.
Notes
Target Framework
I ran into some issues when using the .NET Framework 4 Client Profile as a target framework. I got some strange error saying certain obvious namespaces could not be found, Microsoft.Expression
in my case. If you run into something like this, try setting the target framework to .NET Framework 4 instead of the client version.
Identifiers of Default Panes
Identifier | Type | Title |
---|
Designer_TimelinePane | TimelinePane | Objects and Timeline |
Designer_ToolPane | ToolPane | Tools |
Designer_ProjectPane | ProjectPane | Projects |
Designer_DataPane | DataPane | Data |
Designer_ResourcePane | ResourcePane | Resources |
Designer_PropertyInspector | PropertyInspector | Properties |
Designer_TriggersPane | TriggersPane | Triggers |
Interaction_Skin | SkinView | States |
Designer_AssetPane | AssetPane | Assets |
Interaction_Parts | PartsPane | Parts |
Designer_ResultsPane | ResultsPane | Results |