Click here to Skip to main content
Click here to Skip to main content

MVVM Diagram Designer

, 24 Jan 2013
Rate this:
Please Sign up or sign in to vote.
A WPF diagram designer written with MVVM in mind

Table of Contents

Introduction

A while back a user called "sucram (real name Marcus)"  posted a series of articles here about how to create a diagram designer using WPF. Sucram's original links are as follows:

I remember being truly blown away by this series of articles, as they showed you how to do the following things:

  • Toolbox
  • Drag and Drop
  • Rubber band selection using Adorners
  • Resizing items using Adorners
  • Rotating items using Adorners
  • Connecting items
  • Scrollable designer surface, complete with zoombox

Wow, that sounds fantastic, sounds exactly like the sort of things you would need to create a fully functional diagram designer. Well yeah, it was and still is, but........the thing is I have used WPF a lot, and trying to use the code attached to sucram's series of articles in WPF just wasn't that great. He had taken a very control centric view, in that everything was geared around adding new controls and supplying static styles for said controls.

In reality it was more like working with a WinForms application. Not that there is anything wrong with that, and I really truly do not mean to sound ungrateful, as that could not be further from the truth, without that original series of articles it would have taken me a lot longer to come up with a working diagram designer that I was happy with. So for that I am truly grateful, thanks sucram, you rock.

Anyway, as I say, sucram's original codebase took a very control centric point of view, and added controls using code behind, and held collections of items directly in the diagram surface control. As I say, if that is what you want, cool, however, it was not what I wanted. What I wanted was:

  • All of the features of sucram's original code (actually I didn't want any rotating of items, or resizing of items).
  • A more MVVM driven approach, you know, allow data binding of items, delete items via ICommand etc. etc.
  • Allow me to control the creation of an entire diagram from within a single ViewModel
  • Allow for complex objects to be added to the diagram, i.e., ViewModels that I could style using DataTemplate(s). sucram's original code only allowed simply strings to be used as a DataContext which would control what ImageSource an Image would use to show for a diagram item. I needed my items to be quite rich and allow popups to be shown and associated with the diagram item, such that the data related to the diagram item could be manipulated.
  • Allow me to save the diagram to some backing store.
  • Allow me to load a previously saved diagram from some backing store.

To this end, I have pretty much completely re-written sucram's original code, I think there are probably about two classes that stayed the same, there is now more code, a lot more, however from an end user experience, I think it is now dead easy to control the creation of diagrams from a centralized ViewModel, which allows a diagram to be created via well known WPF paradigms like Binding/DataTemplating.

For example, this is how the attached DemoApp code creates a simple diagram that is shown when you first run the DemoApp:

public partial class Window1 : Window
{
    private Window1ViewModel window1ViewModel;

    public Window1()
    {
        InitializeComponent();

        window1ViewModel = new Window1ViewModel();
        this.DataContext = window1ViewModel;
        this.Loaded += new RoutedEventHandler(Window1_Loaded);
    }

    /// <summary>
    /// This shows you how you can create diagram items in code
    /// </summary>
    void Window1_Loaded(object sender, RoutedEventArgs e)
    {
        SettingsDesignerItemViewModel item1 = new SettingsDesignerItemViewModel();
        item1.Parent = window1ViewModel.DiagramViewModel;
        item1.Left = 100;
        item1.Top = 100;
        window1ViewModel.DiagramViewModel.Items.Add(item1);

        PersistDesignerItemViewModel item2 = new PersistDesignerItemViewModel();
        item2.Parent = window1ViewModel.DiagramViewModel;
        item2.Left = 300;
        item2.Top = 300;
        window1ViewModel.DiagramViewModel.Items.Add(item2);

        ConnectorViewModel con1 = new ConnectorViewModel(item1.RightConnector, item2.TopConnector);
        con1.Parent = window1ViewModel.DiagramViewModel;
        window1ViewModel.DiagramViewModel.Items.Add(con1);
    }
}

As the article progresses, I will show you how to use the new MVVM driven diagram designer classes in your own applications, and you could leave it right there if you wanted to, but if you want to know how it all works, that will be explained in the rest of the article.

What Does It Look Like

This is quite interesting, as if you look at the screenshot below and compare that to the final article that sucram produces, you probably won't see any difference, which as I previously stated was intentional. I think sucram really nailed it, I just wanted a more WPF style codebase, one that supported Binding etc. etc., so yeah I must admit you could easily look at this screenshot and think "Bah humbug......this is exactly the same", well yes, visually speaking I guess it is, however the code is very very different, and the way in which you work with the diagram is very different. Anyway, enough chat, here is a screenshot.

Click the image to see a larger version

So there you go, as I say not much change visually, oh the popup idea to manage a diagram item's data is a new one. We will discuss why I needed this later, and how you too can make use of this mechanism.

Attached Codebase Structure

The attached demo code is split into four projects, which are shown below:

DemoApp
This project is a demonstration project, and is a good example of how to create your own diagram designer. It is a fully functioning demo, and also demonstrates persisting/hydrating using RavenDB which is a NoSQL document database (as I could not be bothered writing loads of SQL).
DemoApp.Persistence.Common Persistence common classes, used by DemoApp.
DemoApp.Persistence.RavenDB
I decided to use RavenDB for persistence which is a NoSQL database that allows raw C# objects to be stored. I decided to do this as I really couldn't be bothered to create all the SQL to save/hydrate diagrams, and I just wanted to get something up and running ASAP.

Though if you use SQL Server/MySQL etc. etc., it should be pretty easy to create the stored procedures/data access layer that talks to your preferred SQL database.
DiagramDesigner This project contains the core classes that are needed to create a diagram in WPF.

How Do I Use It In My Own Applications

This section will talk you through how to create a diagram in your own application. It assumes the following:

  • That you want to use WPF things like Binding/DataTemplating/MVVM
  • You actually want to persist / hydrate diagrams to some backing store (like I say I chose to use RavenDB which is a no SQL document database, but if this is not for you, it should be pretty easy for you to craft your own data access layer talking to your preferred SQL backend)

If you want to create your own MVVM style diagram designer, I have broken it down into seven easy steps, as long as you follow these seven steps to the letter, you should be just fine. There is also a working example of these seven steps by way of the attached DemoApp project code, so you can examine that whilst reading this text, so hopefully you will be OK.

Use It Step 1: Creating the Raw XAML

Here is my bare bones recommended XAML that you should use (providing you go with my recommendation for the Main ViewModel). If you adhere to this recommended XAML/ViewModel, you will get the following features:

  • Automatic toolbox creation
  • New diagram button
  • Save diagram button
  • Load diagram button
  • Progress bar that is shown when you are saving/loaded a diagram

Anyway here is the recommended XAML:

<Window x:Class="DemoApp.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:s="clr-namespace:DiagramDesigner;assembly=DiagramDesigner"
        xmlns:local="clr-namespace:DemoApp"
        WindowState="Maximized"
        SnapsToDevicePixels="True"
        Title="Diagram Designer"        
        Height="850" Width="1100">
    

    <Window.InputBindings>
        <KeyBinding Key="Del"
                    Command="{Binding DeleteSelectedItemsCommand}" />
    </Window.InputBindings>
    
    <DockPanel Margin="0">
        <ToolBar Height="35" DockPanel.Dock="Top">
            <Button ToolTip="New"
                    Content="New"
                    Margin="8,0,3,0"
                    Command="{Binding CreateNewDiagramCommand}"/>
            <Button ToolTip="Save"
                    Content="Save"
                    Margin="8,0,3,0"
                    Command="{Binding SaveDiagramCommand}" />

            <Label Margin="30,0,3,0"
                   VerticalAlignment="Center"
                   Content="Saved Diagrams" />
            <ComboBox Margin="8,0,3,0"
                     Width="200"
                     ToolTip="Saved Diagrams"
                     IsSynchronizedWithCurrentItem="True"
                     ItemsSource="{Binding SavedDiagramsCV}"/>
            <Button ToolTip="Load Selected Diagram"
                    Content="Load"
                    Margin="8,0,3,0"
                    Command="{Binding LoadDiagramCommand}" />
            <ProgressBar Margin="8,0,3,0"
                         Visibility="{Binding Path=IsBusy, 
                Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
                         IsIndeterminate="True"
                         Width="150"
                         Height="20"
                         VerticalAlignment="Center" />


        </ToolBar>
        
        <Grid Margin="0,5,0,0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="230" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>

            <!-- ToolBox Control -->
            <local:ToolBoxControl Grid.Column="0"
                                  DataContext="{Binding ToolBoxViewModel}" />

            <GridSplitter Grid.Column="1"
                          HorizontalAlignment="Left"
                          VerticalAlignment="Stretch"
                          Background="Transparent"
                          Width="3" />
        
            <!-- Diagram Control -->
            <s:DiagramControl Grid.Column="1"
                                  DataContext="{Binding DiagramViewModel}"
                                  Margin="3,1,0,0" />
        </Grid>
    </DockPanel>    
    
</Window>

Use It Step 2: Creating the Main ViewModel

I have taken the liberty of creating a demo ViewModel for you, which I think basically shows you how to do everything you want, so if you follow this example, you should not go too badly wrong.

public class Window1ViewModel : INPCBase
{
    private ObservableCollection<int> savedDiagrams = new ObservableCollection<int>();
    private List<SelectableDesignerItemViewModelBase> itemsToRemove;
    private IMessageBoxService messageBoxService;
    private IDatabaseAccessService databaseAccessService;
    private DiagramViewModel diagramViewModel = new DiagramViewModel();
    private bool isBusy = false;

    public Window1ViewModel()
    {
        messageBoxService = ApplicationServicesProvider.Instance.Provider.MessageBoxService;
        databaseAccessService = ApplicationServicesProvider.Instance.Provider.DatabaseAccessService;

        foreach (var savedDiagram in databaseAccessService.FetchAllDiagram())
        {
            savedDiagrams.Add(savedDiagram.Id);
        }

        ToolBoxViewModel = new ToolBoxViewModel();
        DiagramViewModel = new DiagramViewModel();
        SavedDiagramsCV = CollectionViewSource.GetDefaultView(savedDiagrams);

        DeleteSelectedItemsCommand = new SimpleCommand(ExecuteDeleteSelectedItemsCommand);
        CreateNewDiagramCommand = new SimpleCommand(ExecuteCreateNewDiagramCommand);
        SaveDiagramCommand = new SimpleCommand(ExecuteSaveDiagramCommand);
        LoadDiagramCommand = new SimpleCommand(ExecuteLoadDiagramCommand);
    }

    public SimpleCommand DeleteSelectedItemsCommand { get; private set; }
    public SimpleCommand CreateNewDiagramCommand { get; private set; }
    public SimpleCommand SaveDiagramCommand { get; private set; }
    public SimpleCommand LoadDiagramCommand { get; private set; }
    public ToolBoxViewModel ToolBoxViewModel { get; private set; }
    public ICollectionView SavedDiagramsCV { get; private set; }


    public DiagramViewModel DiagramViewModel
    {
        get
        {
            return diagramViewModel;
        }
        set
        {
            if (diagramViewModel != value)
            {
                diagramViewModel = value;
                NotifyChanged("DiagramViewModel");
            }
        }
    }

    public bool IsBusy
    {
        get
        {
            return isBusy;
        }
        set
        {
            if (isBusy != value)
            {
                isBusy = value;
                NotifyChanged("IsBusy");
            }
        }
    }


    private void ExecuteDeleteSelectedItemsCommand(object parameter)
    {
        itemsToRemove = DiagramViewModel.SelectedItems;
        List<SelectableDesignerItemViewModelBase> connectionsToAlsoRemove = new List<SelectableDesignerItemViewModelBase>();

        foreach (var connector in DiagramViewModel.Items.OfType<ConnectorViewModel>())
        {
            if (ItemsToDeleteHasConnector(itemsToRemove, connector.SourceConnectorInfo))
            {
                connectionsToAlsoRemove.Add(connector);
            }

            if (ItemsToDeleteHasConnector(itemsToRemove, (FullyCreatedConnectorInfo)connector.SinkConnectorInfo))
            {
                connectionsToAlsoRemove.Add(connector);
            }

        }
        itemsToRemove.AddRange(connectionsToAlsoRemove);
        foreach (var selectedItem in itemsToRemove)
        {
            DiagramViewModel.RemoveItemCommand.Execute(selectedItem);
        }
    }

    private void ExecuteCreateNewDiagramCommand(object parameter)
    {
        //ensure that itemsToRemove is cleared ready for any new changes within a session
        itemsToRemove = new List<SelectableDesignerItemViewModelBase>();
        SavedDiagramsCV.MoveCurrentToPosition(-1);
        DiagramViewModel.CreateNewDiagramCommand.Execute(null);
    }

    private void ExecuteSaveDiagramCommand(object parameter)
    {
    ....
    ....
    ....
    }

    private void ExecuteLoadDiagramCommand(object parameter)
    {
    ....
    ....
    ....
    }
}

This example ViewModel shows you how to:

  • Create a bindable list of diagram items / connections
  • How to delete when delete requests come from the view
  • How to save/hydrate a diagram from the database

The only thing you may need to change (if you go for a standard SQL database) is the persistence methods. We will talk more about these later, so hang on we will get to it.

Use It Step 3: Creating Toolbox Item DataTemplates

One important aspect is how the toolbox gets built. So what is the toolbox, I hear you ask?

The toolbox contains the allowable items that you can add to the diagram. This can clearly be seen from the figure below (note that I only allow two items to be created, for brevity).

It may seem straightforward, but this one control is very important, as it dictates the Type of items that are allowed to be added to the diagram.

This ToolBoxControl looks like this:

<UserControl x:Class="DemoApp.ToolBoxControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:s="clr-namespace:DiagramDesigner;assembly=DiagramDesigner"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    
    <Border BorderBrush="LightGray"
            BorderThickness="1">
        <StackPanel>
            <Expander Header="Symbols"
                      IsExpanded="True">
                <ItemsControl ItemsSource="{Binding ToolBoxItems}">
                    <ItemsControl.Template>
                        <ControlTemplate TargetType="{x:Type ItemsControl}">
                            <Border BorderThickness="{TemplateBinding Border.BorderThickness}"
                                    Padding="{TemplateBinding Control.Padding}"
                                    BorderBrush="{TemplateBinding Border.BorderBrush}"
                                    Background="{TemplateBinding Panel.Background}"
                                    SnapsToDevicePixels="True">
                                <ScrollViewer VerticalScrollBarVisibility="Auto">
                                    <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                                </ScrollViewer>
                            </Border>
                        </ControlTemplate>
                    </ItemsControl.Template>
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <WrapPanel Margin="0,5,0,5"
                                       ItemHeight="50"
                                       ItemWidth="50" />
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                    <ItemsControl.ItemContainerStyle>
                        <Style TargetType="{x:Type ContentPresenter}">
                            <Setter Property="Control.Padding"
                                    Value="10" />
                            <Setter Property="ContentControl.HorizontalContentAlignment"
                                    Value="Stretch" />
                            <Setter Property="ContentControl.VerticalContentAlignment"
                                    Value="Stretch" />
                            <Setter Property="ToolTip"
                                    Value="{Binding ToolTip}" />
                            <Setter Property="s:DragAndDropProps.EnabledForDrag"
                                    Value="True" />
                        </Style>
                    </ItemsControl.ItemContainerStyle>
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <Image IsHitTestVisible="True"
                                   Stretch="Fill"
                                   Width="50"
                                   Height="50"
                                   Source="{Binding ImageUrl, Converter={x:Static s:ImageUrlConverter.Instance}}" />
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </Expander>
        </StackPanel>
    </Border>
</UserControl>

The most important part of this by far is the line hat binds the ItemsControl to a property called ToolBoxItems, which is available from the demo ViewModel(s); here is the relevant code:

First we expose a ToolBoxViewModel property from the main DemoApp.Window1ViewModel and then we look into the specifics of the ToolBoxViewModel to see how the ToolBoxItems property provides toolbox items.

public class Window1ViewModel : INPCBase
{
    ToolBoxViewModel = new ToolBoxViewModel();

    public Window1ViewModel()
    {
    ....
    ....

        ToolBoxViewModel = new ToolBoxViewModel();
    ....
    ....
    }
    
    public ToolBoxViewModel ToolBoxViewModel { get; private set; }
}

public class ToolBoxViewModel
{
    private List<ToolBoxData> toolBoxItems = new List<ToolBoxData>();

    public ToolBoxViewModel()
    {
        toolBoxItems.Add(new ToolBoxData("../Images/Setting.png", typeof(SettingsDesignerItemViewModel)));
        toolBoxItems.Add(new ToolBoxData("../Images/Persist.png", typeof(PersistDesignerItemViewModel)));
    }

    public List<ToolBoxData> ToolBoxItems
    {
        get { return toolBoxItems; }
    }
}

The important part of the ToolBoxViewModel is that is stores an Image URL and a Type.

  • The image URL is obviously used to create an image of the required toolbox item Type.
  • The Type is used when you drag and drop a toolbox item onto the design surface, where the toolbox item Type will be instantiated and displayed thanks to a DataTemplate. The Type should be a ViewModel Type that derives from DesignerItemViewModelBase and should probably match something you have in your database.

Use It Step 4: Creating the Diagram Item ViewModels

For the demo app attached, I have ony allowed two different Types of ViewModels, as such you will see exactly two different ToolBox items appearing.

toolBoxItems.Add(new ToolBoxData("../Images/Setting.png", typeof(SettingsDesignerItemViewModel)));
toolBoxItems.Add(new ToolBoxData("../Images/Persist.png", typeof(PersistDesignerItemViewModel)));

These two ViewModels types are:

  • Types that I want to represent in someway on the diagram designer
  • Types that I want to capture extra information about, which can be persisted to the database

Here is what one of these ViewModels looks like (the other one follows the same rules):

public class SettingsDesignerItemViewModel : DesignerItemViewModelBase, ISupportDataChanges
{
    private IUIVisualizerService visualiserService;

    public SettingsDesignerItemViewModel(int id, DiagramViewModel parent, 
                           double left, double top, string setting1)
        : base(id, parent, left, top)
    {

        this.Setting1 = setting1;
        Init();
    }

    public SettingsDesignerItemViewModel()
    {
        Init();
    }

    public String Setting1 { get; set; }
    public ICommand ShowDataChangeWindowCommand { get; private set; }

    public void ExecuteShowDataChangeWindowCommand(object parameter)
    {
        SettingsDesignerItemData data = new SettingsDesignerItemData(Setting1);
        if (visualiserService.ShowDialog(data) == true)
        {
            this.Setting1 = data.Setting1;
        }
    }

    private void Init()
    {
        visualiserService = ApplicationServicesProvider.Instance.Provider.VisualizerService;
        ShowDataChangeWindowCommand = new SimpleCommand(ExecuteShowDataChangeWindowCommand);
        this.ShowConnectors = false;
    }
}

There are quite a few things to note here, so let's go through them one by one:

  1. These diagram item ViewModels must inherit from DesignerItemViewModelBase, which is a class in the core diagram designer code, that needs to know the following things:
    •  Id: Which is used for persistence and maintaining connection relationship for persistence
    • DiagramViewModel: This is the parent DiagramViewModel into which this ViewModel will add itself, once it is dragged onto the designer surface (more on this later)
    • Left: This is the item's Left position on the designer surface
    • Top: This is the item's Top position on the designer surface
    • Setting1: This is a specific bit of data that goes along with this Type of ViewModel. This data and any other specific data would obviously change depending on your actual ViewModel's data requirements
  2. It can be seen that this class also implements a ISupportDataChanges, which is a simple interface that exposes an ICommand that can be used to display a popup Window, to allow the edit of the specific data for this ViewModel Type.    
  3. You can see in the Init() method that we use a ServiceLocation to find a service which is able to show a popup Window which is passed a new ViewModel that represents some data that we wish to edit in a popup Window. ServiceLocation actually makes more sense for this project than a full blown IOC. Due to how to these ViewModels get created and the need for them to know about their parent DiagramViewModel, it just isn't that convenient to use IOC for these diagram item ViewModel(s), it just won't work. You should be able to swap out the IServiceProvider instance on the ApplicationServicesProvider for a test version should you want to, just use the following method:
  4. ApplicationServicesProvider.Instance.SetNewServiceProvider(...);

Use It Step 5: Creating Diagram Item Designer Surface DataTemplates

So we have talked about the ViewModels that underpin the allowable ToolBoxControl items, and we have seen an example of one of these ViewModels, but how does dragging an item from the ToolBoxControl onto the designer surface create the correct diagram item UI components?

The answer to that lies in DataTemplate(s). For each diagram item there must be a matching DataTemplate, this can be seen from the DataTemplate below (which goes hand in hand with the example ViewModel we just saw).

This XAML is located in the file called "SettingsDesignerItemDataTemplate.xaml", which contains the XAML to describe what the diagram item ViewModel should look like in terms of UI controls, and it also describes what the popup Window will look like when you are changing the values of the data for the selected ViewModel (assuming this is something you want to do, which in my opinion is a given, you must be creating a diagram of items, where the items have some data associated with them).

Here is the XAML for the SettingsDesignerItemViewModel:

<!-- DataTemplate for DesignerCanvas look and feel -->
<DataTemplate DataType="{x:Type local:SettingsDesignerItemViewModel}">
        <Grid>
        <Image IsHitTestVisible="False"
                Stretch="Fill"
                Source="../../Images/Setting.png"
                Tag="setting" />
        <Button HorizontalAlignment="Right"
                VerticalAlignment="Bottom"
                Margin="5"
                Template="{StaticResource infoButtonTemplate}"
                Command="{Binding ShowDataChangeWindowCommand}" />
    </Grid>
</DataTemplate>

Which would show the following UI controls, when applied as a DataTemplate:

You will find DataTemplate(s) for all the diagram item ViewModels that I have chosen to support in the DemoApp\Resources\DesignerItems folder. See how there are only two of them, that is because I only chose to support two possible item Types.

You should provide your own resources to match your own ViewModel types here.

Use It Step 6: Creating Diagram Item Popup DataTemplates

As I stated on numerous occasions I do not see the point of a diagram item unless you are going to change some data associated with the item. If this doesn't sound like a requirement you need you can probably gloss over this bit, although the DemoApp code attached is completely geared around the fact that you are able to change the data associated with a given diagram item ViewModel.

We just saw above that we create a specific DataTemplate for a diagram item ViewModel, and we saw the visual UI controls for that DataTemplate contained a Button, and if we examine the relevant part of the item's ViewModel code, we can see that we create a new slim line ViewModel when the Button is clicked. This slim line ViewModel is shown in a generic popup Window (DemoApp\Popups\PopupWindow.xaml) that uses more DataTemplate(s) to decide what visual elements should be shown based on the current DataContext of the generic popup Window.

public void ExecuteShowDataChangeWindowCommand(object parameter)
{
    SettingsDesignerItemData data = new SettingsDesignerItemData(Setting1);
    if (visualiserService.ShowDialog(data) == true)
    {
        this.Setting1 = data.Setting1;
    }
}

This slim line ViewModel simply acts as a property bag that is used to allow edits to the current diagram item ViewModel, providing the user clicks on the OK Button on the generic popup window, the slim line ViewModel property values are applied to the ViewModel that launched the popup Window, otherwise any edits are ignored.

This XAML is located in the file called "SettingsDesignerItemDataTemplate.xaml" which as previously stated contains a DataTemplate for the diagram item ViewModel and also contains a DataTemplate to describe what the slim line ViewModel should look like in the generic popup window. 

Here is the XAML for the SettingsDesignerItemData:

<!-- DataTemplate for Popup look and feel -->
<DataTemplate DataType="{x:Type local:SettingsDesignerItemData}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Label Grid.Row="0"
                Content="Setting1"
                Margin="5" />
        <TextBox Grid.Row="1"
                    HorizontalAlignment="Left"
                    Text="{Binding Setting1}"
                    Width="150"
                    Margin="5" />
    </Grid>
</DataTemplate>

which would show the following UI controls, when applied as a DataTemplate:

Use It Step 7: Persistence

In order to get the most out of this revised diagram designer, I decided one of the core things I wanted to add was the ability to persist/hydrate diagrams to/from a database. For brevity (and my own personal sanity), I have gone down the quickest route possible and chosen to use a No SQL approach in the form of embedded RavenDB.

What Are the Important Things To Save

What are we trying to Save What needs to be saved
PersistDesignerItem

This is specific to the DemoApp, your requirements will be different for sure
This ViewModel is obviously a demo one, however it does show you how to save a diagram item type, and the most important stuff is actually on a base class called DemoApp.Persistence.Common.DesignerItemBase. You should make sure your own persistence ViewModels inherit from that. Anyway, here are all the values that this Type persists:
  • int id (* required by all design items for persistence)
  • double left (* required by all design items for persistence)
  • double top (* required by all design items for persistence)
  • string hostUrl (specific to the demo code)

Where I have made this available in a single class called DemoApp.Persistence.Common.PersistDesignerItem which looks like this:

public class PersistDesignerItem : DesignerItemBase
{
    public PersistDesignerItem(int id, double left, double top, string hostUrl) : base(id, left, top)
    {
        this.HostUrl = hostUrl;
    }

    public string HostUrl { get; set; }

}        
SettingsDesignerItem

This is specific to the DemoApp, your requirements will be different for sure
This ViewModel is obviously a demo one, however it does show you how to save a diagram item type, and the most important stuff is actually on a base class called DemoApp.Persistence.Common.DesignerItemBase. You should make sure your own persistence ViewModels inherit from that. Anyway, here are all the values that this Type persists:
  • int id (* required by all design items for persistence)
  • double left (* required by all design items for persistence)
  • double top (* required by all design items for persistence)
  • string setting1 (specific to the demo code)

Where I have made this available in a single class called DemoApp.Persistence.Common.SettingsDesignerItem which looks like this:

public class SettingsDesignerItem : DesignerItemBase
{
    public SettingsDesignerItem(int id, double left, double top, string setting1)
        : base(id, left, top)
    {
        this.Setting1 = setting1;
    }

    public string Setting1 { get; set; }
}        
Connection
  • int id
  • int sourceId
  • Orientation sourceOrientation
  • Type sourceType
  • int sinkId
  • Orientation sinkOrientation
  • Type sinkType

Where I have made this available in a single class called DemoApp.Persistence.Common.Connection which looks like this:

public class Connection : PersistableItemBase
{
    public Connection(int id, int sourceId, Orientation sourceOrientation, 
        Type sourceType, int sinkId, Orientation sinkOrientation, Type sinkType) : base(id)
    {
        this.SourceId = sourceId;
        this.SourceOrientation = sourceOrientation;
        this.SourceType = sourceType;
        this.SinkId = sinkId;
        this.SinkOrientation = sinkOrientation;
        this.SinkType = sinkType;
    }

    public int SourceId { get; private set; }
    public Orientation SourceOrientation { get; private set; }
    public Type SourceType { get; private set; }
    public int SinkId { get; private set; }
    public Orientation SinkOrientation { get; private set; }
    public Type SinkType { get; private set; }
}
Diagram
List<DiagramItemData> DesignerItems
List<int> ConnectionIds

Where I have made this available in a single class called DemoApp.Persistence.Common.DiagramItem which looks like this:

public class DiagramItem : PersistableItemBase
{
    public DiagramItem() 
    {
        this.DesignerItems = new List<DiagramItemData>();
        this.ConnectionIds = new List<int>();
    }

    public List<DiagramItemData> DesignerItems { get; set; }
    public List<int> ConnectionIds { get; set; }
}

public class DiagramItemData
{
    public DiagramItemData(int itemId, Type itemType)
    {
        this.ItemId = itemId;
        this.ItemType = itemType;
    }

    public int ItemId { get; set; }
    public Type ItemType { get; set; }
}

For those of you that may have an interest in what the RavenDB code looks like, here is the entire class (and yes, this is all of it), sure beats having to create n-many tables, n-many stored procedures, and writing loads of ADO.NET code.

/// <summary>
/// I decided to use RavenDB instead of SQL, to save people having to have SQL Server, and also
/// it just takes less time to do with Raven. This is ALL the CRUD code. Simple no?
/// 
/// Thing is the IDatabaseAccessService and the items it persists could easily be applied to helper methods that
/// use StoredProcedures or ADO code, the data being stored would be exactly the same. You would just need to store
/// the individual property values in tables rather than store objects.
/// </summary>
public class DatabaseAccessService : IDatabaseAccessService
{
    EmbeddableDocumentStore documentStore = null;

    public DatabaseAccessService()
    {
        documentStore = new EmbeddableDocumentStore
        {
            DataDirectory = "Data"
        };
        documentStore.Initialize();
    }
            
    public void DeleteConnection(int connectionId)
    {
        using (IDocumentSession session = documentStore.OpenSession())
        {
            IEnumerable<Connection> conns = session.Query<Connection>().Where(x => x.Id == connectionId);
            foreach (var conn in conns)
            {
                session.Delete<Connection>(conn);
            }
            session.SaveChanges();
        }
    }

    public void DeletePersistDesignerItem(int persistDesignerId)
    {
        using (IDocumentSession session = documentStore.OpenSession())
        {
            IEnumerable<PersistDesignerItem> persistItems = session.Query<PersistDesignerItem>()
                .Where(x => x.Id == persistDesignerId);
            foreach (var persistItem in persistItems)
            {
                session.Delete<PersistDesignerItem>(persistItem);
            }
            session.SaveChanges();
        }
    }

    public void DeleteSettingDesignerItem(int settingsDesignerItemId)
    {
        using (IDocumentSession session = documentStore.OpenSession())
        {
            IEnumerable<SettingsDesignerItem> settingItems = session.Query<SettingsDesignerItem>()
                .Where(x => x.Id == settingsDesignerItemId);
            foreach (var settingItem in settingItems)
            {
                session.Delete<SettingsDesignerItem>(settingItem);
            }
            session.SaveChanges();
        }
    }

    public int SaveDiagram(DiagramItem diagram)
    {
        return SaveItem(diagram);
    }

    public int SavePersistDesignerItem(PersistDesignerItem persistDesignerItemToSave)
    {
        return SaveItem(persistDesignerItemToSave);
    }

    public int SaveSettingDesignerItem(SettingsDesignerItem settingsDesignerItemToSave)
    {
        return SaveItem(settingsDesignerItemToSave);
    }

    public int SaveConnection(Connection connectionToSave)
    {
        return SaveItem(connectionToSave);
    }

    public IEnumerable<DiagramItem> FetchAllDiagram()
    {
        using (IDocumentSession session = documentStore.OpenSession())
        {
            return session.Query<DiagramItem>().ToList();
        }
    }

    public DiagramItem FetchDiagram(int diagramId)
    {
        using (IDocumentSession session = documentStore.OpenSession())
        {
            return session.Query<DiagramItem>().Single(x => x.Id == diagramId);
        }
    }

    public PersistDesignerItem FetchPersistDesignerItem(int settingsDesignerItemId)
    {
        using (IDocumentSession session = documentStore.OpenSession())
        {
            return session.Query<PersistDesignerItem>().Single(x => x.Id == settingsDesignerItemId);
        }
    }

    public SettingsDesignerItem FetchSettingsDesignerItem(int settingsDesignerItemId)
    {
        using (IDocumentSession session = documentStore.OpenSession())
        {
            return session.Query<SettingsDesignerItem>().Single(x => x.Id == settingsDesignerItemId);
        }
    }

    public Connection FetchConnection(int connectionId)
    {
        using (IDocumentSession session = documentStore.OpenSession())
        {
            return session.Query<Connection>().Single(x => x.Id == connectionId);
        }
    }

    private int SaveItem(PersistableItemBase item)
    {
        using (IDocumentSession session = documentStore.OpenSession())
        {
            session.Store(item);
            session.SaveChanges();
        }
        return item.Id;
    }
}

If you want to use something more traditional, such as SQL/MySQL/Oracle etc. etc., you simply need to implement this interface (or come up with your own persistence logic):

public interface IDatabaseAccessService
{
    //delete methods
    void DeleteConnection(int connectionId);
    void DeletePersistDesignerItem(int persistDesignerId);
    void DeleteSettingDesignerItem(int settingsDesignerItemId);

    //save methods
    int SaveDiagram(DiagramItem diagram);
    //PersistDesignerItem is pecific to the DemoApp example
    int SavePersistDesignerItem(PersistDesignerItem persistDesignerItemToSave);
    //SettingsDesignerItem is pecific to the DemoApp example
    int SaveSettingDesignerItem(SettingsDesignerItem settingsDesignerItemToSave);
    int SaveConnection(Connection connectionToSave);

    //Fetch methods
    IEnumerable<DiagramItem> FetchAllDiagram();
    DiagramItem FetchDiagram(int diagramId);
    //PersistDesignerItem is pecific to the DemoApp example
    PersistDesignerItem FetchPersistDesignerItem(int settingsDesignerItemId);
    //SettingsDesignerItem is pecific to the DemoApp example
    SettingsDesignerItem FetchSettingsDesignerItem(int settingsDesignerItemId);
    Connection FetchConnection(int connectionId);
}

Word of Warning

If you do decide to go the traditional SQL route, you will almost certainly need to store the name of the Type instead of the actual Type (that's the power of NoSQL, store me a Type, sure no problem) within the DiagramItem code shown above. You will need to save a string which is the AssemblyQualifiedName of the diagram item Type you are trying to persist. Then to hydrate, you will have to use that Type information to assist you in creating the correct Type again.

Saving/Hydrating a Diagram

Here is the relevant persistence code from the demo app's Window1ViewModel (which is what you should be using as a basis for your own code base). There is obviously some code which I have just shown stubs for below, but hopefully you can get the gist of what is happening based on the names of the methods:

public class Window1ViewModel : INPCBase
{
    //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    // SAVE DIAGRAM
    //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    private void ExecuteSaveDiagramCommand(object parameter)
    {
        if (!DiagramViewModel.Items.Any())
        {
            messageBoxService.ShowError("There must be at least one item in order save a diagram");
            return;
        }

        IsBusy = true;
        DiagramItem wholeDiagramToSave = null;

        Task<int> task = Task.Factory.StartNew<int>(() =>
            {

                if (SavedDiagramId != null)
                {
                    int currentSavedDiagramId = (int)SavedDiagramId.Value;
                    wholeDiagramToSave = databaseAccessService.FetchDiagram(currentSavedDiagramId);

                    //If we have a saved diagram, we need to make sure we clear out all the removed items that
                    //the user deleted as part of this work sesssion
                    foreach (var itemToRemove in itemsToRemove)
                    {
                        DeleteFromDatabase(wholeDiagramToSave, itemToRemove);
                    }
                    //start with empty collections of connections and items, which will be populated based on current diagram
                    wholeDiagramToSave.ConnectionIds = new List<int>();
                    wholeDiagramToSave.DesignerItems = new List<DiagramItemData>();
                }
                else
                {
                    wholeDiagramToSave = new DiagramItem();
                }

                //ensure that itemsToRemove is cleared ready for any new changes within a session
                itemsToRemove = new List<SelectableDesignerItemViewModelBase>();

                //Save all PersistDesignerItemViewModel
                foreach (var persistItemVM in DiagramViewModel.Items.OfType<PersistDesignerItemViewModel>())
                {
                    PersistDesignerItem persistDesignerItem = new PersistDesignerItem(persistItemVM.Id, 
                        persistItemVM.Left, persistItemVM.Top, persistItemVM.HostUrl);
                    persistItemVM.Id = databaseAccessService.SavePersistDesignerItem(persistDesignerItem);
                    wholeDiagramToSave.DesignerItems.Add(new DiagramItemData(persistDesignerItem.Id, typeof(PersistDesignerItem)));
                }
                //Save all PersistDesignerItemViewModel
                foreach (var settingsItemVM in DiagramViewModel.Items.OfType<SettingsDesignerItemViewModel>())
                {
                    SettingsDesignerItem settingsDesignerItem = new SettingsDesignerItem(settingsItemVM.Id, 
                       settingsItemVM.Left, settingsItemVM.Top, settingsItemVM.Setting1);
                    settingsItemVM.Id = databaseAccessService.SaveSettingDesignerItem(settingsDesignerItem);
                    wholeDiagramToSave.DesignerItems.Add(new DiagramItemData(settingsDesignerItem.Id, typeof(SettingsDesignerItem)));
                }
                //Save all connections which should now have their Connection.DataItems filled in with correct Ids
                foreach (var connectionVM in DiagramViewModel.Items.OfType<ConnectorViewModel>())
                {
                    FullyCreatedConnectorInfo sinkConnector = connectionVM.SinkConnectorInfo as FullyCreatedConnectorInfo;

                    Connection connection = new Connection(
                        connectionVM.Id,
                        connectionVM.SourceConnectorInfo.DataItem.Id,
                        GetOrientationFromConnector(connectionVM.SourceConnectorInfo.Orientation),
                        GetTypeOfDiagramItem(connectionVM.SourceConnectorInfo.DataItem),
                        sinkConnector.DataItem.Id,
                        GetOrientationFromConnector(sinkConnector.Orientation),
                        GetTypeOfDiagramItem(sinkConnector.DataItem));

                    connectionVM.Id = databaseAccessService.SaveConnection(connection);
                    wholeDiagramToSave.ConnectionIds.Add(connectionVM.Id);
                }

                wholeDiagramToSave.Id = databaseAccessService.SaveDiagram(wholeDiagramToSave);
                return wholeDiagramToSave.Id;
            });
        task.ContinueWith((ant) =>
        {
            int wholeDiagramToSaveId = ant.Result;
            if (!savedDiagrams.Contains(wholeDiagramToSaveId))
            {
                List<int> newDiagrams = new List<int>(savedDiagrams);
                newDiagrams.Add(wholeDiagramToSaveId);
                SavedDiagrams = newDiagrams;

            }
            IsBusy = false;
            messageBoxService.ShowInformation(string.Format("Finished saving Diagram Id : {0}", wholeDiagramToSaveId));

        }, TaskContinuationOptions.OnlyOnRanToCompletion);
    }

    //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    // LOAD DIAGRAM
    //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    private void ExecuteLoadDiagramCommand(object parameter)
    {
        IsBusy = true;
        DiagramItem wholeDiagramToLoad = null;
        if (SavedDiagramId == null)
        {
            messageBoxService.ShowError("You need to select a diagram to load");
            return;
        }

        Task<DiagramViewModel> task = Task.Factory.StartNew<DiagramViewModel>(() =>
            {
                //ensure that itemsToRemove is cleared ready for any new changes within a session
                itemsToRemove = new List<SelectableDesignerItemViewModelBase>();
                DiagramViewModel diagramViewModel = new DiagramViewModel();

                wholeDiagramToLoad = databaseAccessService.FetchDiagram((int)SavedDiagramId.Value);

                //load diagram items
                foreach (DiagramItemData diagramItemData in wholeDiagramToLoad.DesignerItems)
                {
                    if (diagramItemData.ItemType == typeof(PersistDesignerItem))
                    {
                        PersistDesignerItem persistedDesignerItem = databaseAccessService.FetchPersistDesignerItem(diagramItemData.ItemId);
                        PersistDesignerItemViewModel persistDesignerItemViewModel =
                            new PersistDesignerItemViewModel(persistedDesignerItem.Id, diagramViewModel, 
                             persistedDesignerItem.Left, persistedDesignerItem.Top, persistedDesignerItem.HostUrl);
                        diagramViewModel.Items.Add(persistDesignerItemViewModel);
                    }
                    if (diagramItemData.ItemType == typeof(SettingsDesignerItem))
                    {
                        SettingsDesignerItem settingsDesignerItem = databaseAccessService.FetchSettingsDesignerItem(diagramItemData.ItemId);
                        SettingsDesignerItemViewModel settingsDesignerItemViewModel =
                            new SettingsDesignerItemViewModel(settingsDesignerItem.Id, diagramViewModel, 
                               settingsDesignerItem.Left, settingsDesignerItem.Top, settingsDesignerItem.Setting1);
                        diagramViewModel.Items.Add(settingsDesignerItemViewModel);
                    }
                }
                //load connection items
                foreach (int connectionId in wholeDiagramToLoad.ConnectionIds)
                {
                    Connection connection = databaseAccessService.FetchConnection(connectionId);

                    DesignerItemViewModelBase sourceItem = GetConnectorDataItem(diagramViewModel, connection.SourceId, connection.SourceType);
                    ConnectorOrientation sourceConnectorOrientation = GetOrientationForConnector(connection.SourceOrientation);
                    FullyCreatedConnectorInfo sourceConnectorInfo = GetFullConnectorInfo(connection.Id, sourceItem, sourceConnectorOrientation);

                    DesignerItemViewModelBase sinkItem = GetConnectorDataItem(diagramViewModel, connection.SinkId, connection.SinkType);
                    ConnectorOrientation sinkConnectorOrientation = GetOrientationForConnector(connection.SinkOrientation);
                    FullyCreatedConnectorInfo sinkConnectorInfo = GetFullConnectorInfo(connection.Id, sinkItem, sinkConnectorOrientation);

                    ConnectorViewModel connectionVM = new ConnectorViewModel(connection.Id, 
                        diagramViewModel, sourceConnectorInfo, sinkConnectorInfo);
                    diagramViewModel.Items.Add(connectionVM);
                }

                return diagramViewModel;
            });
        task.ContinueWith((ant) =>
            {
                this.DiagramViewModel = ant.Result;
                IsBusy = false;
                messageBoxService.ShowInformation(string.Format("Finished loading Diagram Id : {0}", wholeDiagramToLoad.Id));
 
            },TaskContinuationOptions.OnlyOnRanToCompletion);
    }

    private FullyCreatedConnectorInfo GetFullConnectorInfo(int connectorId, 
        DesignerItemViewModelBase dataItem, ConnectorOrientation connectorOrientation)
    {
        ....
        ....
        ....
    }

    private Type GetTypeOfDiagramItem(DesignerItemViewModelBase vmType)
    {
        ....
        ....
        ....
    }

    private DesignerItemViewModelBase GetConnectorDataItem(DiagramViewModel diagramViewModel, 
        int conectorDataItemId, Type connectorDataItemType)
    {
        ....
        ....
        ....
    }

    private Orientation GetOrientationFromConnector(ConnectorOrientation connectorOrientation)
    {
        ....
        ....
        ....
    }

    private ConnectorOrientation GetOrientationForConnector(Orientation persistedOrientation)
    {
        ....
        ....
        ....
    }

    private bool ItemsToDeleteHasConnector(List<SelectableDesignerItemViewModelBase> itemsToRemove, 
        FullyCreatedConnectorInfo connector)
    {
        ....
        ....
        ....
    }

    private void DeleteFromDatabase(DiagramItem wholeDiagramToAdjust, 
                 SelectableDesignerItemViewModelBase itemToDelete)
    {
        ....
        ....
        ....
    }
}

So that concludes the steps in how to use this in your own application, and if that is all you are after, you can take a deep breath, and call it a day. However should you wish to know more about how it all works under the bonnet, please read on. 

How Does The Diagram Designer Stuff Actually Work

This section will discuss some of the nitty gritty about how the diagram designer works (in a MVVM manner, that is). I should just mention a couple of things:

  • There are a few parts of the application that will require further investigation by you. To explain every detail would take me all year.
  • There are certain elements (granted not many) that have not changed from sucram's original code, as such these areas will not be explained, as you can get an understanding of these elements from sucram's original articles.
  • There are some things that are just assumed, such as:
    • A working knowledege of key WPF concepts, such as
      • VisualTree manipulation
      • Bindings
      • Attached properties
      • DataContext
      • Styles
      • DataTemplates

Drag And Drop To the Design Surface

We have already seen part of this puzzle when we looked at the ToolBoxViewModel which exposed the Types that the diagram supports. Just to remind ourselves of that, let's have a look at the ViewModel code:

public class ToolBoxViewModel
{
    private List<ToolBoxData> toolBoxItems = new List<ToolBoxData>();

    public ToolBoxViewModel()
    {
        toolBoxItems.Add(new ToolBoxData("../Images/Setting.png", typeof(SettingsDesignerItemViewModel)));
        toolBoxItems.Add(new ToolBoxData("../Images/Persist.png", typeof(PersistDesignerItemViewModel)));
    }

    public List<ToolBoxData> ToolBoxItems
    {
        get { return toolBoxItems; }
    }
}

This ViewModel code goes hand in hand with a ToolBoxControl (which I have decided to include in the DemoApp project just in case you do not like its look and feel), this code is shown below.

<UserControl x:Class="DemoApp.ToolBoxControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:s="clr-namespace:DiagramDesigner;assembly=DiagramDesigner"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    
    <Border BorderBrush="LightGray"
            BorderThickness="1">
        <StackPanel>
            <Expander Header="Symbols"
                      IsExpanded="True">
                <ItemsControl ItemsSource="{Binding ToolBoxItems}">
                    <ItemsControl.Template>
                        <ControlTemplate TargetType="{x:Type ItemsControl}">
                            <Border BorderThickness="{TemplateBinding Border.BorderThickness}"
                                    Padding="{TemplateBinding Control.Padding}"
                                    BorderBrush="{TemplateBinding Border.BorderBrush}"
                                    Background="{TemplateBinding Panel.Background}"
                                    SnapsToDevicePixels="True">
                                <ScrollViewer VerticalScrollBarVisibility="Auto">
                                    <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                                </ScrollViewer>
                            </Border>
                        </ControlTemplate>
                    </ItemsControl.Template>
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <WrapPanel Margin="0,5,0,5"
                                       ItemHeight="50"
                                       ItemWidth="50" />
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                    <ItemsControl.ItemContainerStyle>
                        <Style TargetType="{x:Type ContentPresenter}">
                            <Setter Property="Control.Padding"
                                    Value="10" />
                            <Setter Property="ContentControl.HorizontalContentAlignment"
                                    Value="Stretch" />
                            <Setter Property="ContentControl.VerticalContentAlignment"
                                    Value="Stretch" />
                            <Setter Property="ToolTip"
                                    Value="{Binding ToolTip}" />
                            <Setter Property="s:DragAndDropProps.EnabledForDrag"
                                    Value="True" />
                        </Style>
                    </ItemsControl.ItemContainerStyle>
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <Image IsHitTestVisible="True"
                                   Stretch="Fill"
                                   Width="50"
                                   Height="50"
                                   Source="{Binding ImageUrl, Converter={x:Static s:ImageUrlConverter.Instance}}" />
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </Expander>
        </StackPanel>
    </Border>
</UserControl>

The most important thing in ToolBoxControl.xaml is the the following line:

<Setter Property="s:DragAndDropProps.EnabledForDrag" Value="True" />

This line registers an attached property which provides some drag and drop loveliness for the ToolBoxControl. Let's have a look at the code in that attached property, shall we?

public static class DragAndDropProps     
{
    #region EnabledForDrag

    public static readonly DependencyProperty EnabledForDragProperty =
        DependencyProperty.RegisterAttached("EnabledForDrag", typeof(bool), typeof(DragAndDropProps),
            new FrameworkPropertyMetadata((bool)false,
                new PropertyChangedCallback(OnEnabledForDragChanged)));

    public static bool GetEnabledForDrag(DependencyObject d)
    {
        return (bool)d.GetValue(EnabledForDragProperty);
    }

    public static void SetEnabledForDrag(DependencyObject d, bool value)
    {
        d.SetValue(EnabledForDragProperty, value);
    }

    private static void OnEnabledForDragChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement fe = (FrameworkElement) d;


        if((bool)e.NewValue)
        {
            fe.PreviewMouseDown += Fe_PreviewMouseDown;
            fe.MouseMove += Fe_MouseMove;
        }
        else
        {
            fe.PreviewMouseDown -= Fe_PreviewMouseDown;
            fe.MouseMove -= Fe_MouseMove;
        }
    }
    #endregion

    #region DragStartPoint

    public static readonly DependencyProperty DragStartPointProperty =
        DependencyProperty.RegisterAttached("DragStartPoint", typeof(Point?), typeof(DragAndDropProps));

    public static Point? GetDragStartPoint(DependencyObject d)
    {
        return (Point?)d.GetValue(DragStartPointProperty);
    }


    public static void SetDragStartPoint(DependencyObject d, Point? value)
    {
        d.SetValue(DragStartPointProperty, value);
    }

    #endregion

    static void Fe_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
    {
        Point? dragStartPoint = GetDragStartPoint((DependencyObject)sender);

        if (e.LeftButton != MouseButtonState.Pressed)
            dragStartPoint = null;

        if (dragStartPoint.HasValue)
        {
            DragObject dataObject = new DragObject();
            dataObject.ContentType = (((FrameworkElement)sender).DataContext as ToolBoxData).Type;
            dataObject.DesiredSize = new Size(65, 65);
            DragDrop.DoDragDrop((DependencyObject)sender, dataObject, DragDropEffects.Copy);
            e.Handled = true;
        }
    }

    static void Fe_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        SetDragStartPoint((DependencyObject)sender, e.GetPosition((IInputElement)sender));
    }
}

It can be seen that this code hooks up various mouse events for the item that is being dragged, which allows items to be moved, but we are more interested in how things actually make it on to the designer surface for now. So how does that occur? Well, it is all down to these couple of lines, where we grab the Type from the item that is being dragged, which is done by examining its DataContext, which we know is bound to a ToolBoxData object.

DragObject dataObject = new DragObject();
dataObject.ContentType = (((FrameworkElement)sender).DataContext as ToolBoxData).Type;
dataObject.DesiredSize = new Size(65, 65);
DragDrop.DoDragDrop((DependencyObject)sender, dataObject, DragDropEffects.Copy);
e.Handled = true;

So that is one part, that is the drag part, so now for the drop. To see how the drop works, we need to examine the DesignerCanvas which is a control that is used as part of the overall DiagramDesigner.DiagramControl styling. Here is the relevant portion of the DesinerCanvas

public class DesignerCanvas : Canvas
{
    public DesignerCanvas()
    {
        this.AllowDrop = true;
    ....
    ....
    ....
    }

    ....
    ....
    ....

    protected override void OnDrop(DragEventArgs e)
    {
        base.OnDrop(e);
        DragObject dragObject = e.Data.GetData(typeof(DragObject)) as DragObject;
        if (dragObject != null)
        {
            (DataContext as IDiagramViewModel).ClearSelectedItemsCommand.Execute(null);
            Point position = e.GetPosition(this);
            DesignerItemViewModelBase itemBase = (DesignerItemViewModelBase)Activator.CreateInstance(dragObject.ContentType);
            itemBase.Left = Math.Max(0, position.X - DesignerItemViewModelBase.ItemWidth / 2);
            itemBase.Top = Math.Max(0, position.Y - DesignerItemViewModelBase.ItemHeight / 2);
            itemBase.IsSelected = true;
            (DataContext as IDiagramViewModel).AddItemCommand.Execute(itemBase);
        }
        e.Handled = true;
    }
}

It can be seen that the DesignerCanvas knows about its parent ViewModel, yes that's right, that would be the DiagramViewModel.

The DiagramViewModel is the ViewModel that the DiagramControl uses as its DataContext, so when new items are added via the DiagramCanvas, these are automatically shown thanks to WPF excellent Binding support, and the designer item DataTemplates that we looked previously in the section Use It Step 5: Creating Diagram Item Designer Surface DataTemplates are used to make sure the correct UI elements are created for the actual diagram item Types that are added.

Binding the Items Collection

This is probably the area I am most proud of. Let me explain why. When working with this diagramming control, it is totally feasible to have the following:

  • Type X diagram item
  • Type Y diagram item
  • Connection from X to Y

Now what I wanted was to have a single collection of "Items" that the DiagramControl could bind to. So I guess you could think about using inheritance here such that all of the diagram items inherit from a common base class. That could work. In fact that does solve 1/2 the puzzle, so each and every diagram item Type including ConnectorViewModel (which we have not seen just yet) inherits from SelectableDesignerItemViewModelBase. By using inheritance we are able to create a single "Items" list from the DiagramViewModel, which is as follows:

public class DiagramViewModel : INPCBase, IDiagramViewModel
{
    private ObservableCollection<SelectableDesignerItemViewModelBase> items = 
        new ObservableCollection<SelectableDesignerItemViewModelBase>();

    public DiagramViewModel()
    {
    .....
    .....
    .....
    .....

    }
 
    public ObservableCollection<SelectableDesignerItemViewModelBase> Items
    {
        get { return items; }
    }
}

We can happily bind a standard ItemsControl to this, which is what we do within the XAML for the DiagramControl.

<UserControl x:Class="DiagramDesigner.DiagramControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:s="clr-namespace:DiagramDesigner"
             xmlns:c="clr-namespace:DiagramDesigner.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    
    <Border BorderBrush="LightGray"
            BorderThickness="1">
        <Grid>
            <ScrollViewer Name="DesignerScrollViewer"
                          Background="Transparent"
                          HorizontalScrollBarVisibility="Auto"
                          VerticalScrollBarVisibility="Auto">

                <ItemsControl ItemsSource="{Binding Items}"
                              ItemContainerStyleSelector="{x:Static s:DesignerItemsControlItemStyleSelector.Instance}">
        ......
        ......
        ......
        ......
        ......
        ......
        ......
   
                </ItemsControl>


            </ScrollViewer>
           </Grid>
    </Border>
</UserControl>

Great, so we are able to bind an ItemsControl to a single ObservableCollection<SelectableDesignerItemViewModelBase>, but how does that enable us to change the visual appearance of these items? After all a ConnectionViewModel simply must look different from a diagram item. Well yeah, sure it does, how does that work? Voodoo?

Well, the secret to that lies in the use of a specialized StyleSelector which you can see being set above to an instance of DesignerItemsControlItemStyleSelector, which works as follows:

public class DesignerItemsControlItemStyleSelector : StyleSelector
{
    static DesignerItemsControlItemStyleSelector()
    {
        Instance = new DesignerItemsControlItemStyleSelector();
    }

    public static DesignerItemsControlItemStyleSelector Instance
    {
        get;
        private set;
    }


    public override Style SelectStyle(object item, DependencyObject container)
    {
        ItemsControl itemsControl = ItemsControl.ItemsControlFromItemContainer(container);
        if (itemsControl == null)
            throw new InvalidOperationException("DesignerItemsControlItemStyleSelector : Could not find ItemsControl");

        if(item is DesignerItemViewModelBase)
        {

            return (Style)itemsControl.FindResource("designerItemStyle");
        }
            
        if (item is ConnectorViewModel)
        {
            return (Style)itemsControl.FindResource("connectorItemStyle");
        }

        return null;
    }
} 

We use the Type of the item to determine which Style to look for and apply it to the item being bound, where we are expecting to find these Styles in the Resources section of the parent ItemsControl. Where this can be seen below in a more detailed snippet of the DesignerControl.xaml, where it can clearly be seen that there are two styles in play here:

  • designerItemStyle: Which is applied to any bound item of Type DesignerItemViewModelBase
  • connectorItemStyle: Which is applied to any item of Type ConnectorViewModel
<ItemsControl ItemsSource="{Binding Items}"
                ItemContainerStyleSelector="{x:Static s:DesignerItemsControlItemStyleSelector.Instance}">
    <ItemsControl.Resources>

        <Style x:Key="designerItemStyle" TargetType="{x:Type ContentPresenter}">
            <Setter Property="Canvas.Top"
                    Value="{Binding Top}" />
            <Setter Property="Canvas.Left"
                    Value="{Binding Left}" /><    
            <Setter Property="s:SelectionProps.EnabledForSelection"
                    Value="True" />
            <Setter Property="s:ItemConnectProps.EnabledForConnection"
                    Value="True" />
            <Setter Property="Width"
                    Value="{x:Static  s:DesignerItemViewModelBase.ItemWidth}" />
            <Setter Property="Height"
                    Value="{x:Static  s:DesignerItemViewModelBase.ItemHeight}" />
        ....
        ....
        ....
        ....

         </Style>

        <Style x:Key="connectorItemStyle"
                TargetType="{x:Type ContentPresenter}">
            <Setter Property="Width"
                    Value="{Binding Area.Width}" />
            <Setter Property="Height"
                    Value="{Binding Area.Height}" />
            <Setter Property="Canvas.Top"
                    Value="{Binding Area.Top}" />
            <Setter Property="Canvas.Left"
                    Value="{Binding Area.Left}" />
            <Setter Property="s:SelectionProps.EnabledForSelection"
                    Value="True" />
        ....
        ....
        ....
        ....

        </Style>


    </ItemsControl.Resources>

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <s:DesignerCanvas Loaded="DesignerCanvas_Loaded"
                                MinHeight="800"
                                MinWidth="1000"
                                Background="White"
                                AllowDrop="True">
            </s:DesignerCanvas>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

</ItemsControl>

Adding Connections

Adding connections is achieved by firstly Mouse over(ing) a diagram control, which will show four Connector objects. This can be seen from the Style that is applied to the bound diagram items.

<ItemsControl ItemsSource="{Binding Items}"
                ItemContainerStyleSelector="{x:Static s:DesignerItemsControlItemStyleSelector.Instance}">
    <ItemsControl.Resources>

        <Style x:Key="designerItemStyle"
                TargetType="{x:Type ContentPresenter}">
            ....
            ....
            ....
            ....
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <Grid x:Name="selectedGrid">
                            <c:DragThumb x:Name="PART_DragThumb"
                                            Cursor="SizeAll" />
                            <ContentPresenter x:Name="PART_ContentPresenter"
                                                HorizontalAlignment="Stretch"
                                                VerticalAlignment="Stretch"
                                                Content="{TemplateBinding Content}" />
                            <Grid Margin="-5"
                                    x:Name="PART_ConnectorDecorator">
                                <s:Connector DataContext="{Binding LeftConnector}"
                                                Orientation="Left"
                                                VerticalAlignment="Center"
                                                HorizontalAlignment="Left"
                                                Visibility="{Binding Path=ShowConnectors, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
                                <s:Connector DataContext="{Binding TopConnector}"
                                                Orientation="Top"
                                                VerticalAlignment="Top"
                                                HorizontalAlignment="Center"
                                                Visibility="{Binding Path=ShowConnectors, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
                                <s:Connector DataContext="{Binding RightConnector}"
                                                Orientation="Right"
                                                VerticalAlignment="Center"
                                                HorizontalAlignment="Right"
                                                Visibility="{Binding Path=ShowConnectors, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
                                <s:Connector DataContext="{Binding BottomConnector}"
                                                Orientation="Bottom"
                                                VerticalAlignment="Bottom"
                                                HorizontalAlignment="Center"
                                                Visibility="{Binding Path=ShowConnectors, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
                            </Grid>
                        </Grid>
                        <DataTemplate.Triggers>
                            <Trigger Property="IsMouseOver"
                                        Value="true">
                                <Setter TargetName="PART_ConnectorDecorator"
                                        Property="Visibility"
                                        Value="Visible" />
                            </Trigger>

                            <DataTrigger Value="True"
                                            Binding="{Binding RelativeSource={RelativeSource Self},Path=IsDragConnectionOver}">
                                <Setter TargetName="PART_ConnectorDecorator"
                                        Property="Visibility"
                                        Value="Visible" />
                            </DataTrigger>
                ....
                ....
                ....
                ....
                ....
                            </DataTrigger>
                        </DataTemplate.Triggers>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ItemsControl.Resources>

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <s:DesignerCanvas Loaded="DesignerCanvas_Loaded"
                                MinHeight="800"
                                MinWidth="1000"
                                Background="White"
                                AllowDrop="True">
            </s:DesignerCanvas>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

</ItemsControl>

It can be seen that there are four Connectors shown above. Each Connector also has its DataContext bound to the parent diagram item, such that when an actual ConnectorViewModel is created we known which parents the connection is between. Here is what the four Connectors look like:

So that is how the four Connectors are shown, but how are the connections actually made?

Well, to understand that we need to look at the DesignerCanvas code, which is shown below.

The basic idea is a simple one, we need to perform a HitTest when we MouseUp and if we have a positive HitTest for a sink diagram item, then we have enough information to create a full connection.

public class DesignerCanvas : Canvas
{
    private ConnectorViewModel partialConnection;
    private List<Connector> connectorsHit = new List<Connector>();
    private Connector sourceConnector;


    public Connector SourceConnector
    {
        get { return sourceConnector; }
        set
        {
            if (sourceConnector != value)
            {
                sourceConnector = value;
                connectorsHit.Add(sourceConnector);
                FullyCreatedConnectorInfo sourceDataItem = sourceConnector.DataContext as FullyCreatedConnectorInfo;
                    

                Rect rectangleBounds = sourceConnector.TransformToVisual(this).TransformBounds(new Rect(sourceConnector.RenderSize));
                Point point = new Point(rectangleBounds.Left + (rectangleBounds.Width / 2),
                                        rectangleBounds.Bottom + (rectangleBounds.Height / 2));
                partialConnection = new ConnectorViewModel(sourceDataItem, new PartCreatedConnectionInfo(point));
                sourceDataItem.DataItem.Parent.AddItemCommand.Execute(partialConnection);
            }
        }
    }

    protected override void OnMouseUp(MouseButtonEventArgs e)
    {
        base.OnMouseUp(e);

        Mediator.Instance.NotifyColleagues<bool>("DoneDrawingMessage", true);

        if (sourceConnector != null)
        {
            FullyCreatedConnectorInfo sourceDataItem = sourceConnector.DataContext as FullyCreatedConnectorInfo;
            if (connectorsHit.Count() == 2)
            {
                Connector sinkConnector = connectorsHit.Last();
                FullyCreatedConnectorInfo sinkDataItem = sinkConnector.DataContext as FullyCreatedConnectorInfo;

                int indexOfLastTempConnection = sinkDataItem.DataItem.Parent.Items.Count - 1;
                sinkDataItem.DataItem.Parent.RemoveItemCommand.Execute(
                    sinkDataItem.DataItem.Parent.Items[indexOfLastTempConnection]);
                sinkDataItem.DataItem.Parent.AddItemCommand.Execute(new ConnectorViewModel(sourceDataItem, sinkDataItem));
            }
            else
            {
                //Need to remove last item as we did not finish drawing the path
                int indexOfLastTempConnection = sourceDataItem.DataItem.Parent.Items.Count - 1;
                sourceDataItem.DataItem.Parent.RemoveItemCommand.Execute(
                    sourceDataItem.DataItem.Parent.Items[indexOfLastTempConnection]);
            }
        }
        connectorsHit = new List<Connector>();
        sourceConnector = null;
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);

        if(SourceConnector != null)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                Point currentPoint = e.GetPosition(this);
                partialConnection.SinkConnectorInfo = new PartCreatedConnectionInfo(currentPoint);
                HitTesting(currentPoint);
            }
        }
        else
        {
        //rubber band selection
            ....
            ....
            ....
            ....

        }
        e.Handled = true;
    }

    private void HitTesting(Point hitPoint)
    {
        DependencyObject hitObject = this.InputHitTest(hitPoint) as DependencyObject;
        while (hitObject != null &&
                hitObject.GetType() != typeof(DesignerCanvas))
        {
            if (hitObject is Connector)
            {
                if (!connectorsHit.Contains(hitObject as Connector))
                    connectorsHit.Add(hitObject as Connector);
            }
            hitObject = VisualTreeHelper.GetParent(hitObject);
        }
    }
}

But what about when we are attempting to connect two diagram item Connectors, but have not (as yet) made a full connection, what should we do in this case?

Well, we still need to make a connection, it's just for this type of connection, we only know the source diagram item and not the sink diagram item, so we can still draw a connection line, but not the terminating sink arrow. To support this operation, the following things happen:

  • On MouseMove we create a new ConnectorViewModel which has a FullyCreatedConnectorInfo source and also has a ConnectorInfoBase sink. Where in reality the sink connector will be a PartCreatedConnectorInfo.
    • Where the FullyCreatedConnectorInfo information allows us to draw a line from an actual diagram item Connector item
    • Where the ConnectorInfoBase (PartCreatedConnectorInfo) information only allows us to draw a line to a given point (the current Mouse position)
  • On MouseUp if we have a positive HitTest for a diagram item Connector the last ConnectorViewModel is removed and replaced with a new ConnectorViewModel with both the source and the sink being FullyCreatedConnectorInfo objects. Which allows us to create a full connection (one that should remain on the diagram) between two actual diagram items.
  • On MouseUp if we have a negative HitTest for a diagram item the last ConnectorViewModel is removed, as we have not selected a valid sink diagram item Connector, and have moused up in empty space. As such the partial connection we have drawn should not remain on the diagram.

All of this may make a bit more sense once you see what the various ViewModels and ConnectorViewModel Styles look like. So let's have a look at those now, shall we?

ConnectorInfoBase/PartCreatedConnectorInfo

This class is the base class for one end of a connection, and provides the minimum set of data required to represent an end of a connection. We use this for an end where we do not (as yet) know the actual diagram item Connector we have hit (which may never happen).

public enum ConnectorOrientation
{
    None    =   0,
    Left    =   1,
    Top     =   2,
    Right   =   3,
    Bottom  =   4
}

public abstract class ConnectorInfoBase : INPCBase
{

    private static double connectorWidth = 8;
    private static double connectorHeight = 8;

    public ConnectorInfoBase(ConnectorOrientation orientation)
    {
        this.Orientation = orientation;
    }

    public ConnectorOrientation Orientation { get; private set; }

    public static double ConnectorWidth
    {
        get { return connectorWidth; }
    }

    public static double ConnectorHeight
    {
        get { return connectorHeight; }
    }
}


public class PartCreatedConnectionInfo : ConnectorInfoBase
{
    public Point CurrentLocation { get; private set; }

    public PartCreatedConnectionInfo(Point currentLocation) : base(ConnectorOrientation.None)
    {
        this.CurrentLocation = currentLocation;
    }
}

FullyCreatedConnectorInfo

This class is the class we use when we know the actual associated diagram item associated with the Connector, and provides the full set of data required to represent an end of a connection to an actual diagram item Connector.

public class FullyCreatedConnectorInfo : ConnectorInfoBase
{
    private bool showConnectors = false;

    public FullyCreatedConnectorInfo(DesignerItemViewModelBase dataItem, ConnectorOrientation orientation)
        : base(orientation)
    {
        this.DataItem = dataItem;
    }

    public DesignerItemViewModelBase DataItem { get; private set; }

    public bool ShowConnectors
    {
        get
        {
            return showConnectors;
        }
        set
        {
            if (showConnectors != value)
            {
                showConnectors = value;
                NotifyChanged("ShowConnectors");
            }
        }
    }
}

ConnectorViewModel

This class holds two ConnectorInfoBase connectors at any one time, that could be:

  • 2 x FullyCreatedConnectorInfo
  • 1 x FullCreatedConnectorInfo and 1 x PartCreatedConnectorInfo
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows;
using DiagramDesigner.Helpers;

namespace DiagramDesigner
{
    public class ConnectorViewModel : SelectableDesignerItemViewModelBase
    {
        private FullyCreatedConnectorInfo sourceConnectorInfo;
        private ConnectorInfoBase sinkConnectorInfo;
        private Point sourceB;
        private Point sourceA;
        private List<Point> connectionPoints;
        private Point endPoint;
        private Rect area;


        public ConnectorViewModel(int id, IDiagramViewModel parent, 
            FullyCreatedConnectorInfo sourceConnectorInfo, FullyCreatedConnectorInfo sinkConnectorInfo) : base(id,parent)
        {
            Init(sourceConnectorInfo, sinkConnectorInfo);
        }

        public ConnectorViewModel(FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo)
        {
            Init(sourceConnectorInfo, sinkConnectorInfo);
        }


        public static IPathFinder PathFinder { get; set; }

        public bool IsFullConnection
        {
            get { return sinkConnectorInfo is FullyCreatedConnectorInfo; }
        }

        public Point SourceA
        {
            get
            {
                return sourceA;
            }
            set
            {
                if (sourceA != value)
                {
                    sourceA = value;
                    UpdateArea();
                    NotifyChanged("SourceA");
                }
            }
        }

        public Point SourceB
        {
            get
            {
                return sourceB;
            }
            set
            {
                if (sourceB != value)
                {
                    sourceB = value;
                    UpdateArea();
                    NotifyChanged("SourceB");
                }
            }
        }

        public List<Point> ConnectionPoints
        {
            get
            {
                return connectionPoints;
            }
            private set
            {
                if (connectionPoints != value)
                {
                    connectionPoints = value;
                    NotifyChanged("ConnectionPoints");
                }
            }
        }

        public Point EndPoint
        {
            get
            {
                return endPoint;
            }
            private set
            {
                if (endPoint != value)
                {
                    endPoint = value;
                    NotifyChanged("EndPoint");
                }
            }
        }

        public Rect Area
        {
            get
            {
                return area;
            }
            private set
            {
                if (area != value)
                {
                    area = value;
                    UpdateConnectionPoints();
                    NotifyChanged("Area");
                }
            }
        }

        public ConnectorInfo ConnectorInfo(ConnectorOrientation orientation, double left, double top, Point position)
        {

            return new ConnectorInfo()
            {
                Orientation = orientation,
                DesignerItemSize = new Size(DesignerItemViewModelBase.ItemWidth, DesignerItemViewModelBase.ItemHeight),
                DesignerItemLeft = left,
                DesignerItemTop = top,
                Position = position

            };
        }

        public FullyCreatedConnectorInfo SourceConnectorInfo
        {
            get
            {
                return sourceConnectorInfo;
            }
            set
            {
                if (sourceConnectorInfo != value)
                {

                    sourceConnectorInfo = value;
                    SourceA = PointHelper.GetPointForConnector(this.SourceConnectorInfo);
                    NotifyChanged("SourceConnectorInfo");
                    (sourceConnectorInfo.DataItem as INotifyPropertyChanged).PropertyChanged 
                += new WeakINPCEventHandler(ConnectorViewModel_PropertyChanged).Handler;
                }
            }
        }

        public ConnectorInfoBase SinkConnectorInfo
        {
            get
            {
                return sinkConnectorInfo;
            }
            set
            {
                if (sinkConnectorInfo != value)
                {

                    sinkConnectorInfo = value;
                    if (SinkConnectorInfo is FullyCreatedConnectorInfo)
                    {
                        SourceB = PointHelper.GetPointForConnector((FullyCreatedConnectorInfo)SinkConnectorInfo);
                        (((FullyCreatedConnectorInfo)sinkConnectorInfo).DataItem as INotifyPropertyChanged).PropertyChanged 
                += new WeakINPCEventHandler(ConnectorViewModel_PropertyChanged).Handler;
                    }
                    else
                    {

                        SourceB = ((PartCreatedConnectionInfo)SinkConnectorInfo).CurrentLocation;
                    }
                    NotifyChanged("SinkConnectorInfo");
                }
            }
        }

        private void UpdateArea()
        {
            Area = new Rect(SourceA, SourceB); 
        }

        private void UpdateConnectionPoints()
        {
            ConnectionPoints = new List<Point>()
                                   {
                                       
                                       new Point( SourceA.X  <  SourceB.X ? 0d : Area.Width, SourceA.Y  <  SourceB.Y ? 0d : Area.Height ), 
                                       new Point(SourceA.X  >  SourceB.X ? 0d : Area.Width, SourceA.Y  >  SourceB.Y ? 0d : Area.Height)
                                   };

            ConnectorInfo sourceInfo = ConnectorInfo(SourceConnectorInfo.Orientation,
                                            ConnectionPoints[0].X,
                                            ConnectionPoints[0].Y,
                                            ConnectionPoints[0]);

            if(IsFullConnection)
            {
                EndPoint = ConnectionPoints.Last();
                ConnectorInfo sinkInfo = ConnectorInfo(SinkConnectorInfo.Orientation,
                                  ConnectionPoints[1].X,
                                  ConnectionPoints[1].Y,
                                  ConnectionPoints[1]);

                ConnectionPoints = PathFinder.GetConnectionLine(sourceInfo, sinkInfo, true);
            }
            else
            {
                ConnectionPoints = PathFinder.GetConnectionLine(sourceInfo, ConnectionPoints[1], ConnectorOrientation.Left);
                EndPoint = new Point();
            }
        }

        private void ConnectorViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "Left":
                case "Top":
                    SourceA = PointHelper.GetPointForConnector(this.SourceConnectorInfo);
                    if (this.SinkConnectorInfo is FullyCreatedConnectorInfo)
                    {
                        SourceB = PointHelper.GetPointForConnector((FullyCreatedConnectorInfo)this.SinkConnectorInfo);
                    }
                    break;

            }
        }

        private void Init(FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo)
        {
            this.Parent = sourceConnectorInfo.DataItem.Parent;
            this.SourceConnectorInfo = sourceConnectorInfo;
            this.SinkConnectorInfo = sinkConnectorInfo;
            PathFinder = new OrthogonalPathFinder();
        }

    }
}

The last piece of the puzzle is down to the Style for the ConnectorViewModel which dictates what the connection looks like, this is shown below:

<Style x:Key="connectorItemStyle"
        TargetType="{x:Type ContentPresenter}">
    <Setter Property="Width"
            Value="{Binding Area.Width}" />
    <Setter Property="Height"
            Value="{Binding Area.Height}" />
    <Setter Property="Canvas.Top"
            Value="{Binding Area.Top}" />
    <Setter Property="Canvas.Left"
            Value="{Binding Area.Left}" />
    <Setter Property="s:SelectionProps.EnabledForSelection"
            Value="True" />
    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate>
                <Canvas Margin="0"
                        x:Name="selectedGrid"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch">
                    <Polyline x:Name="poly"
                                Stroke="Gray"
                                Points="{Binding Path=ConnectionPoints, Converter={x:Static s:ConnectionPathConverter.Instance}}"
                                StrokeThickness="2" />


                    <Path x:Name="arrow"
                            Data="M0,10 L5,0 10,10 z"
                            Visibility="{Binding Path=IsFullConnection, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
                            Fill="Gray"
                            HorizontalAlignment="Left"
                            Height="10"
                            Canvas.Left="{Binding EndPoint.X}"
                            Canvas.Top="{Binding EndPoint.Y}"
                            Stretch="Fill"
                            Stroke="Gray"
                            VerticalAlignment="Top"
                            Width="10"
                            RenderTransformOrigin="0.5,0.5">
                        <Path.RenderTransform>
                            <RotateTransform x:Name="rot" />
                        </Path.RenderTransform>
                    </Path>
                </Canvas>
                <DataTemplate.Triggers>
                    <DataTrigger Value="True"
                                    Binding="{Binding IsSelected}">
                        <Setter TargetName="poly"
                                Property="Stroke"
                                Value="Black" />
                        <Setter TargetName="arrow"
                                Property="Stroke"
                                Value="Black" />
                        <Setter TargetName="arrow"
                                Property="Fill"
                                Value="Black" />
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=SinkConnectorInfo.Orientation}"
                                    Value="Left">
                        <Setter TargetName="arrow"
                                Property="Margin"
                                Value="-15,-5,0,0" />
                        <Setter TargetName="arrow"
                                Property="RenderTransform">
                            <Setter.Value>
                                <RotateTransform Angle="90" />
                            </Setter.Value>
                        </Setter>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=SinkConnectorInfo.Orientation}"
                                    Value="Top">
                        <Setter TargetName="arrow"
                                Property="Margin"
                                Value="-5,-15,0,0" />
                        <Setter TargetName="arrow"
                                Property="RenderTransform">
                            <Setter.Value>
                                <RotateTransform Angle="180" />
                            </Setter.Value>
                        </Setter>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=SinkConnectorInfo.Orientation}"
                                    Value="Right">
                        <Setter TargetName="arrow"
                                Property="Margin"
                                Value="5,-5,0,0" />
                        <Setter TargetName="arrow"
                                Property="RenderTransform">
                            <Setter.Value>
                                <RotateTransform Angle="-90" />
                            </Setter.Value>
                        </Setter>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=SinkConnectorInfo.Orientation}"
                                    Value="Bottom">
                        <Setter TargetName="arrow"
                                Property="Margin"
                                Value="-5,10,0,0" />
                        <Setter TargetName="arrow"
                                Property="RenderTransform">
                            <Setter.Value>
                                <RotateTransform Angle="0" />
                            </Setter.Value>
                        </Setter>
                    </DataTrigger>

                </DataTemplate.Triggers>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

It can be seen from this Style that we use a ConnectionPathConverter ValueConverter to convert from the List<Point> in the ConnectorViewModel to a PathSegmentCollection which is used to draw the actual connection Path:

[ValueConversion(typeof(List<Point>), typeof(PathSegmentCollection))]
public class ConnectionPathConverter : IValueConverter
{
    static ConnectionPathConverter()
    {
        Instance = new ConnectionPathConverter();
    }

    public static ConnectionPathConverter Instance
    {
        get;
        private set;
    }

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        List<Point> points = (List<Point>)value;
        PointCollection pointCollection = new PointCollection();
        foreach (Point point in points)
        {
            pointCollection.Add(point);   
        }
        return pointCollection;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Important note

If you don't like the way that connections path List<Point> are found (which I have not shown you as it is a lot of abstract nonsensical code), you can change that to your own algorithm, simply implement the IPathFinder interface which looks like this:

public interface IPathFinder
{
    List<Point> GetConnectionLine(ConnectorInfo source, ConnectorInfo sink, bool showLastLine);
    List<Point> GetConnectionLine(ConnectorInfo source, Point sinkPoint, ConnectorOrientation preferredOrientation);
}

I have provided a default implementation of this for you, which is available in the OrthogonalPathFinder class. Anyway you can either use the default or swap it for your own by using the static method on the ConnectorViewModel where there is an example of in the DemoApp.Window1ViewModel, which is shown here using the default path finder that you will find attached to this article. This path finding work was mainly done by sucram, I can't take any credit for it, it works well most of the time, but could be better, so if you see fit, write your own and swap it out.

public Window1ViewModel()
{
    //OrthogonalPathFinder is a pretty bad attempt at finding path points,
    //it just shows you, you can swap this out with relative
    //ease if you wish just create a new IPathFinder class and pass it in right here
    ConnectorViewModel.PathFinder = new OrthogonalPathFinder();
}

Easy, no?

Selection/Deselection

The selection/deselection comes in two flavours.

Flavour 1: Standard Mouse Down Selection

This form of selection/deselection is pretty simple and is mainly driven from PreviewMouseDown events received, however the logic I have provided also takes into account CTRL + SHIFT keyboard modifiers. This is a standard attached property that can be applied by simply setting a value for this attached property as such:

<Style x:Key="designerItemStyle" TargetType="{x:Type ContentPresenter}">
    ....
    ....
    ....
    <Setter Property="s:SelectionProps.EnabledForSelection" Value="True" />
    ....
    ....
    ....
</style>

Here is the full code for this attached property, it is pretty simple, we just listen to PreviewMouseDown events and also takes into account CTRL + SHIFT modifiers, and select the item in the DiagramViewModel if it should be selected.

public static class SelectionProps  
{
    #region EnabledForSelection

    public static readonly DependencyProperty EnabledForSelectionProperty =
        DependencyProperty.RegisterAttached("EnabledForSelection", typeof(bool), typeof(SelectionProps),
            new FrameworkPropertyMetadata((bool)false,
                new PropertyChangedCallback(OnEnabledForSelectionChanged)));

    public static bool GetEnabledForSelection(DependencyObject d)
    {
        return (bool)d.GetValue(EnabledForSelectionProperty);
    }

    public static void SetEnabledForSelection(DependencyObject d, bool value)
    {
        d.SetValue(EnabledForSelectionProperty, value);
    }

    private static void OnEnabledForSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement fe = (FrameworkElement)d;
        if ((bool)e.NewValue)
        {
            fe.PreviewMouseDown += Fe_PreviewMouseDown;
        }
        else
        {
            fe.PreviewMouseDown -= Fe_PreviewMouseDown;
        }
    }

    #endregion

    static void Fe_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        SelectableDesignerItemViewModelBase selectableDesignerItemViewModelBase = 
            (SelectableDesignerItemViewModelBase)((FrameworkElement)sender).DataContext;

        if(selectableDesignerItemViewModelBase != null)
        {
            if ((Keyboard.Modifiers & (ModifierKeys.Shift | ModifierKeys.Control)) != ModifierKeys.None)
            {
                if ((Keyboard.Modifiers & (ModifierKeys.Shift)) != ModifierKeys.None)
                {
                    selectableDesignerItemViewModelBase.IsSelected = !selectableDesignerItemViewModelBase.IsSelected;
                }

                if ((Keyboard.Modifiers & (ModifierKeys.Control)) != ModifierKeys.None)
                {
                    selectableDesignerItemViewModelBase.IsSelected = !selectableDesignerItemViewModelBase.IsSelected;
                }
            }
            else if (!selectableDesignerItemViewModelBase.IsSelected)
            {
                foreach (SelectableDesignerItemViewModelBase item in selectableDesignerItemViewModelBase.Parent.SelectedItems)
                    selectableDesignerItemViewModelBase.IsSelected = false;

                selectableDesignerItemViewModelBase.Parent.SelectedItems.Clear();
                selectableDesignerItemViewModelBase.IsSelected = true;
            }
        }
    }
}

When an item is selected it is shown with a drop shadow as shown below:

When a connection is selected it is shown with a Black Brush as shown below:

Flavour 2: Rubberband Selection

Rubber band selection is done using the AdornerLayer where we simply check whether a SelectableDesignerItemViewModelBase (that is items and connections, as they both inherit from the base class) bounds is within the current rubber band rectangle, and if so make the appropriate selection.

Here is the relevant code for the RubberbandAdorner:

public class RubberbandAdorner : Adorner
{
    private Point? startPoint;
    private Point? endPoint;
    private Pen rubberbandPen;

    private DesignerCanvas designerCanvas;

    public RubberbandAdorner(DesignerCanvas designerCanvas, Point? dragStartPoint)
        : base(designerCanvas)
    {
        this.designerCanvas = designerCanvas;
        this.startPoint = dragStartPoint;
        rubberbandPen = new Pen(Brushes.LightSlateGray, 1);
        rubberbandPen.DashStyle = new DashStyle(new double[] { 2 }, 1);
    }

    protected override void OnMouseMove(System.Windows.Input.MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            if (!this.IsMouseCaptured)
                this.CaptureMouse();

            endPoint = e.GetPosition(this);
            UpdateSelection();
            this.InvalidateVisual();
        }
        else
        {
            if (this.IsMouseCaptured) this.ReleaseMouseCapture();
        }

        e.Handled = true;
    }

    protected override void OnMouseUp(System.Windows.Input.MouseButtonEventArgs e)
    {
        // release mouse capture
        if (this.IsMouseCaptured) this.ReleaseMouseCapture();

        // remove this adorner from adorner layer
        AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this.designerCanvas);
        if (adornerLayer != null)
            adornerLayer.Remove(this);

        e.Handled = true;
    }

    protected override void OnRender(DrawingContext dc)
    {
        base.OnRender(dc);

        // without a background the OnMouseMove event would not be fired !
        // Alternative: implement a Canvas as a child of this adorner, like
        // the ConnectionAdorner does.
        dc.DrawRectangle(Brushes.Transparent, null, new Rect(RenderSize));

        if (this.startPoint.HasValue && this.endPoint.HasValue)
            dc.DrawRectangle(Brushes.Transparent, rubberbandPen, new Rect(this.startPoint.Value, this.endPoint.Value));
    }


    private T GetParent<T>(Type parentType, DependencyObject dependencyObject) where T : DependencyObject
    {
        DependencyObject parent = VisualTreeHelper.GetParent(dependencyObject);
        if (parent.GetType() == parentType)
            return (T)parent;
        return GetParent<T>(parentType, parent);
    }

    private void UpdateSelection()
    {
        IDiagramViewModel vm = (designerCanvas.DataContext as IDiagramViewModel);
        Rect rubberBand = new Rect(startPoint.Value, endPoint.Value);
        ItemsControl itemsControl = GetParent<ItemsControl>(typeof (ItemsControl), designerCanvas);

        foreach (SelectableDesignerItemViewModelBase item in vm.Items)
        {
            if (item is SelectableDesignerItemViewModelBase)
            {
                DependencyObject container = itemsControl.ItemContainerGenerator.ContainerFromItem(item);

                Rect itemRect = VisualTreeHelper.GetDescendantBounds((Visual) container);
                Rect itemBounds = ((Visual) container).TransformToAncestor(designerCanvas).TransformBounds(itemRect);

                if (rubberBand.Contains(itemBounds))
                {
                    item.IsSelected = true;
                }
                else
                {
                    if (!(Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)))
                    {
                        item.IsSelected = false;
                    }
                }
            }
        }
    }
}

Here is a screenshot of the RubberbandAdorner in action:

The eagle eyed amongst you may be wondering how this RubberbandAdorner actually gets created in the first place. Well, that is actually done in the Mouse event overrides in the DesignerCanvas (which is where the RubberbandAdorner rectangle start point would originate if you think about it). Here is the relevant code:

public class DesignerCanvas : Canvas
{
    ....
    ....
    ....
    ....
    protected override void OnMouseDown(MouseButtonEventArgs e)
    {
        base.OnMouseDown(e);

        if (e.LeftButton == MouseButtonState.Pressed)
        {
            //if we are source of event, we are rubberband selecting
            if (e.Source == this)
            {
                // in case that this click is the start for a 
                // drag operation we cache the start point
                rubberbandSelectionStartPoint = e.GetPosition(this);

                IDiagramViewModel vm = (this.DataContext as IDiagramViewModel);
                if (!(Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)))
                {
                    vm.ClearSelectedItemsCommand.Execute(null);
                }
                e.Handled = true;
            }
        }
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);

        if(SourceConnector != null)
        {
            ....
            ....
            ....
            ....

        }
        else
        {
            // if mouse button is not pressed we have no drag operation, ...
            if (e.LeftButton != MouseButtonState.Pressed)
                rubberbandSelectionStartPoint = null;

            // ... but if mouse button is pressed and start
            // point value is set we do have one
            if (this.rubberbandSelectionStartPoint.HasValue)
            {
                // create rubberband adorner
                AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this);
                if (adornerLayer != null)
                {
                    RubberbandAdorner adorner = new RubberbandAdorner(this, rubberbandSelectionStartPoint);
                    if (adorner != null)
                    {
                        adornerLayer.Add(adorner);
                    }
                }
            }
        }
        e.Handled = true;
    }
}

Deleting Items

Once objects are selected, it is possible to delete them by pressing the DEL (delete) key. This simply removes all selected items from the ViewModel that exposes the actual diagram items which, thanks to our specialized StyleSelector that we saw earlier, actually means both designer items and connections.

The delete journey starts in the DemoApp.Window1.xaml demo code, where we use a simple KeyBinding:

<Window x:Class="DemoApp.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:s="clr-namespace:DiagramDesigner;assembly=DiagramDesigner"
        xmlns:local="clr-namespace:DemoApp"
        WindowState="Maximized"
        SnapsToDevicePixels="True"
        Title="Diagram Designer"        
        Height="850" Width="1100">

    <Window.InputBindings>
        <KeyBinding Key="Del"
                    Command="{Binding DeleteSelectedItemsCommand}" />
    </Window.InputBindings>

    ....
    ....
    ....
    ....


</Window>

This KeyBinding simply fires an ICommand in the demo app ViewModel DemoApp.Window1ViewModel, which if you remember is my recommended way of creating your own working code. So let's look at that DemoApp.Window1ViewModel code now, here are the relevant parts of the DemoApp.Window1ViewModel code:

public class Window1ViewModel : INPCBase
{
    .....
    .....
    .....
    .....
    private void ExecuteDeleteSelectedItemsCommand(object parameter)
    {
        itemsToRemove = DiagramViewModel.SelectedItems;
        List<SelectableDesignerItemViewModelBase> connectionsToAlsoRemove = 
        new List<SelectableDesignerItemViewModelBase>();

        foreach (var connector in DiagramViewModel.Items.OfType<ConnectorViewModel>())
        {
            if (ItemsToDeleteHasConnector(itemsToRemove, connector.SourceConnectorInfo))
            {
                connectionsToAlsoRemove.Add(connector);
            }

            if (ItemsToDeleteHasConnector(itemsToRemove, (FullyCreatedConnectorInfo)connector.SinkConnectorInfo))
            {
                connectionsToAlsoRemove.Add(connector);
            }

        }
        itemsToRemove.AddRange(connectionsToAlsoRemove);
        foreach (var selectedItem in itemsToRemove)
        {
            DiagramViewModel.RemoveItemCommand.Execute(selectedItem);
        }
    }
    .....
    .....
    .....
    .....
}

It can be seen that the DemoApp.Window1ViewModel basically does the following

  • Loops through each ConnectorViewModel and determines if they are connected to something that the user has asked to delete
    • If a connection is found to an item that is being requested to delete, the connection is obviously junk, and should also be deleted
  • Does a delete in the database, remember persistence is something I added, you need to handle this how you need it, for me this just meant deleting something from a RavenDB, this is done in the DemoApp.Window1ViewModel
  • Delegates the real deleting of the items to the DiagramViewModel (whose code we will now look at)

The final code that is run in response to a DELETE key being pressed is that the following ICommand is run in the DiagramViewModel:

public class DiagramViewModel : INPCBase, IDiagramViewModel
{
    private ObservableCollection<SelectableDesignerItemViewModelBase> items = 
        new ObservableCollection<SelectableDesignerItemViewModelBase>();

    .....
    .....
    .....


    public SimpleCommand RemoveItemCommand { get; private set; }

    public ObservableCollection<SelectableDesignerItemViewModelBase> Items
    {
        get { return items; }
    }

    public List<SelectableDesignerItemViewModelBase> SelectedItems
    {
        get { return Items.Where(x => x.IsSelected).ToList(); }
    }


    private void ExecuteRemoveItemCommand(object parameter)
    {
        if (parameter is SelectableDesignerItemViewModelBase)
        {
            SelectableDesignerItemViewModelBase item = (SelectableDesignerItemViewModelBase)parameter;
            items.Remove(item);
        }
    }
    .....
    .....
    .....

}

And thanks to the fact that we are using a Binding that refers to a ObservableCollection<SelectableDesignerItemViewModelBase> (OK, we use our special DesignerItemsControlItemStyleSelector StyleSelector that we saw earlier) the diagram designer just updates.

Simple, right?

That's It For Now

Anyway that's it for now folks, hope you enjoyed this one, I quite enjoyed making this proper with WPF.

I am off on holiday now, so may not answer any questions until I get back. However if you liked this, a vote or comment is always appreciated. Oh by the way, when I come back from hols, I am straight into a new Node.Js/D3.js article. Fun times.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionDraw Connector during runtime Pinmemberreymond8-Apr-13 18:15 
AnswerRe: Draw Connector during runtime PinmvpSacha Barber8-Apr-13 19:45 
GeneralRe: Draw Connector during runtime Pinmemberreymond8-Apr-13 20:38 
GeneralRe: Draw Connector during runtime PinmvpSacha Barber8-Apr-13 21:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140814.1 | Last Updated 24 Jan 2013
Article Copyright 2012 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid