Microsoft introduced WPF in .NET 3.0 in November, 2006. Microsoft's code name for WPF was "Avalon". Microsoft has released four major WPF versions since its introduction: WPF 3.5 (Nov 2007), WPF 3.5sp1 (Aug 2008), WPF 4 (April 2010), and WPF 4.5 (August 2012). .NET 4.0 saw huge improvements in performance and text rendering quality due to requests from the Visual Studio 2010 team as they were incorporating WPF into the VS 2010 product. Windows XP support has been dropped in .NET 4.5., which is the version CONNECT Edition is using.

Rather than relying on the older GDI/GDI+ subsystem, WPF utilizes DirectX for its rendering. WPF attempts to provide a consistent programming model for building applications and provides a separation between the user interface and the business logic. WPF employs XAML (eXtensible Application Markup Language), an XML-based language, to define and link various UI elements. XAML resembles similar XML-oriented object models, such as those implemented in XUL and SVG.

WPF aims to unify a number of common user interface elements, such as 2D/3D rendering, fixed and adaptive documents, typography, vector graphics, runtime animation, and pre-rendered media. These elements can then be linked and manipulated based on various events, user interactions, and data bindings.

WPF runtime libraries are included with all versions of Microsoft Windows since Windows Vista and Windows Server 2008. Users of Windows XP SP2/SP3 and Windows Server 2003 can optionally install the necessary libraries.

For more information from Microsoft on WPF, see http://msdn.microsoft.com/en-us/library/ms754130(v=vs.100).aspx

WPF Support in PowerPlatform

Support for WPF has been in added in CONNECT Edition, alongside support for MDL, WinForms and MFC. WPF Support in PowerPlatform is mainly located in classes in the Bentley.MstnPlatformNET.WPF namespace. WPF Windows are now "First-class citizens" within PowerPlatform. WPF content can be attached to PowerPlatform as floating windows, dockable windows, dockable toolbars and to the Tool Settings window. When attached to PowerPlatform the following features are correctly handled, as they are for MDL, WinForms and MFC:

The following classes are located in the Bentley.MstnPlatformNET.WPF namespace in Bentley.MicroStation.WPF.dll:

Model-View-ViewModel (MVVM) Support

Model-View-ViewModel Pattern (MVVM) is very popular among WPF and Silverlight developers. It is similar to the Model-View-Controller (MVC) and Model-View-Presenter (MVP) patterns but utilizes the strong WPF Data Binding, Commanding and Dependency Property features. Use of the MVVM pattern leads to a good separation of concerns and the ability to unit test the ViewModels and Models without use of the UI in the Views. For a good introduction and history of the MVVM pattern, the MSDN Magazine article by Josh Smith is a must read: https://msdn.microsoft.com/en-us/magazine/dd419663.aspx

Numerous MVVM toolkits exist, from the simpler libraries like the one Josh introduced in the article above to "Caliburn.Micro", which is quite complex. We are providing a library of MVVM base classes, located in Bentley.UI.dll and in the Bentley.UI.Mvvm namespace, which are similar to the classes described in Josh's article. They are simpler and "more pure" than those found in the more complicated libraries. These classes include:

Note that, while we encourage you to use the MVVM pattern and these MVVM classes in particular, you are not locked into using them. If you prefer to use another set of MVVM classes, feel free to do so. We also encourage you to discuss this topic further with us to make our MVVM classes the best they can be.

AddIn Setup for WPF

To use a WPF window or content in PowerPlatform, an AddIn (PowerPlatform .NET Application) must be developed. This is also the case for WinForms. C# is the main language used in AddIns, but VB.NET and C++/CLI may also be used. An AddIn is actually a .NET Class Library. It is not a WPF application nor a WinForms application. A System.Windows.Application object is instantiated by PowerPlatform, and its Resources property may be used by .NET AddIns.

MKE File

The Bmake MKE file for the AddIn can build the SLN file via the MSBuild task. The following example shows the correct setup for building the SLN file. The value of the macro is passed into the build and is used in the CSPROJ files for Bentley DLL references. Depending on your situation, you may need to use a different macro name to point to your proper SubParts location. The build of the SLN file is invoked via the MSBuild task using the macro. Symlinks are created after the build for Transkit purposes. In this sample, the Properties subdirectory is specified as the source location since it contains the transkit.xml file and the application's RESX and *.Designer.cs files.

appName = WPFSample
%include mdl.mki
%include $(SharedMki)stdversion.mki
#--------------------------------------------------------------------------------------------------
# General Configuration
#--------------------------------------------------------------------------------------------------
ProjectTitle = $(appName)
SolutionName = $(appName).sln
MSB-OutputPath=$(o)
MSB-IntermediateOutputPath = $(o)
MSB-BaseIntermediateOutputPath = $(o)
buildSolution:
~mkdir $(o)
|[== Building $(ProjectTitle) ==]
~task msbuild $(CommonMSBuildOpts) -i:Projects=$(baseDir)$(SolutionName) -p:ContextSubPartsAssembliesDir=$(ContextSubPartsAssembliesDir)
#----------------------------------------------------------------------
# Symlink the transkit directory into the BuildContext
#----------------------------------------------------------------------
always:
@CreateSymLinks.py -d"$(ContextDeliveryDir)Transkit\TestApps\$(appName)=$(baseDir)Properties"

To open an AddIn solution/project, you should use the bmake command with the -dIDE option. This sets up an environment with certain required macros.

bmake -dIDE WPFSample.mke

CSPROJ File

References to DLLs are specified in the CSPROJ file. Because we need to use macros when specifying the locations of Bentley DLLs, references to these DLLs must be hand-edited in the CSPROJ file. To illustrate this, here are a couple of references to Bentley DLLs:

<Reference Include="$(ContextSubPartsAssembliesDir)ustation.dll">
<Private>false</Private>
</Reference>
<Reference Include="$(ContextSubPartsAssembliesDir)Bentley.MicroStation.dll">
<Private>false</Private>
</Reference>
<Reference Include="$(ContextSubPartsAssembliesDir)Bentley.MicroStation.WPF.dll">
<Private>false</Private>
</Reference>

At a minimum, the following DLLs need to be referenced by a WPF AddIn:

AddIn.cs

All .NET AddIns must contain a public subclass of the Bentley.MstnPlatformNET.AddIn class. The *.AddIn.cs file contains this required class.

namespace WPFSample
{
//====================================================================================
//==============+===============+===============+===============+===============+======
[Bentley.MstnPlatformNET.AddInAttribute(
MdlTaskID = "WPFSample",
KeyinTree = "WPFSample.commands.xml"
)]
public class WPFSampleApp : Bentley.MstnPlatformNET.AddIn
{
private static WPFSampleApp s_WPFSampleApp;
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
private WPFSampleApp
(
IntPtr mdlDesc
)
: base(mdlDesc)
{
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
protected override int Run
(
string[] commandLine
)
{
// save a reference to our addin to prevent it from being garbage collected.
s_WPFSampleApp = this;
return 0;
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
internal static WPFSampleApp Instance
{
get { return s_WPFSampleApp; }
}
} // WPFSampleApp
} // namespace WPFSample

Keyins

Keyins, or commands, for the AddIn are defined in an XML file, which conforms to the http://www.bentley.com/schemas/1.0/MicroStation/AddIn/KeyinTree.xsd schema. The XML file is added to the AddIn 's project as a DeflatedEmbeddedResource in the CSPROJ file.

CSPROJ File Excerpt for the Command.XML File

<ItemGroup>
<DeflatedEmbeddedResource Include="WPFSample.commands.xml">
<ManifestName>WPFSample.commands.xml.Deflate</ManifestName>
<SubType>Designer</SubType>
</DeflatedEmbeddedResource>
</ItemGroup>

Example of a Commands.XML File

<?xml version="1.0" encoding="utf-8" ?>
<KeyinTree xmlns="http://www.bentley.com/schemas/1.0/MicroStation/AddIn/KeyinTree.xsd">
<RootKeyinTable ID="root">
<Keyword SubtableRef="props" CommandClass="MacroCommand" CommandWord="WPFSAMPLE">
<Options Required="true"/>
</Keyword>
</RootKeyinTable>
<SubKeyinTables>
<KeyinTable ID="props">
<Keyword CommandWord="OPEN" />
<Keyword CommandWord="CLOSE" />
</KeyinTable>
</SubKeyinTables>
<KeyinHandlers>
<KeyinHandler Keyin="WPFSAMPLE OPEN" Function="WPFSample.Keyins.Open" />
<KeyinHandler Keyin="WPFSAMPLE CLOSE" Function="WPFSample.Keyins.Close" />
</KeyinHandlers>
</KeyinTree>

Example of a Keyins.cs File

The associated methods for each keyin in the AddIn may be centralized in a separate C# file, as this example shows:

namespace WPFSample
{
internal class Keyins
{
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
internal static void Open
(
string unparsed
)
{
// An OpenWindow static method in the Views.FloatWindow class is called to handle the keyin
Views.FloatWindow.OpenWindow (unparsed);
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
internal static void Close
(
string unparsed
)
{
// A CloseWindow static method in the Views.FloatWindow class is called to handle the keyin
Views.FloatWindow.CloseWindow (unparsed);
}
}
}

MVVM Setup

A popular part of setting up a project for the MVVM pattern is to create three subdirectories, one for the "Models", one for for the "Views" and another for the "ViewModels". This will also affect the namespace for each of the components in the MVVM pattern. By separating out the components into their own subdirectories, the further leads to a separation of concerns, which is one of the benefits of the MVVM pattern.

Internationalization / Localization

An AddIn is integrated into the Transkit process with the following:

PartFile Part Element

The <Part> element for your AddIn application has a <Bindings><Assemblies> element that may contain a <TransKit> element along with the built DLL names. This element specifies the location in the Transkit where the application's transkit source files are located.

<Part Name="TestApp-WPFSample" BMakeFile="mstn\testapps\WPFSample\WPFSample.mke">
<Bindings>
<Assemblies ProductDirectoryName="TestApps">
Delivery\WPFSample.dll
<TransKit SourceDirectory="Delivery\Transkit\TestApps\WPFSample" />
</Assemblies>
</Bindings>
</Part>

MKE SymLinks associating Transkit to Properties

The MKE file for the AddIn application should create SymLinks for the Transkit location that points to the Properties subdirectory, which contains the transkit.xml file, RESX files and the generated *.Designer.cs files.

#----------------------------------------------------------------------
# Symlink the transkit directory into the BuildContext
#----------------------------------------------------------------------
always:
@CreateSymLinks.py -d"$(ContextDeliveryDir)Transkit\TestApps\$(appName)=$(baseDir)Properties"

transkit.xml file

To integrate a RESX file into the Bentley transkit, a transkit.xml file must exist in the Properties subdirectory, which is referenced by a SymLink in the MKE file.

<?xml version="1.0" encoding="utf-8"?>
<Transkit>
<SatelliteAssembly DllName="WPFSample" DeliverEnglish="False" RightsCompliant="False">
<ResxFile ResourceName="WPFSample.Properties.Resources" FileName="Resources.resx" />
</SatelliteAssembly>
</Transkit>

Properties / RESX Files

AddIns can be internationalized by putting string into RESX files with resource IDs then accessing those strings via the IDs rather than hard-coding strings in both XAML and C# source. To create a RESX file, go the Properties page for the AddIn project, click on the "Resources" tab then click on the link on that page. This will create a Resources.resx file. This RESX file may be renamed afterwards, but the correct name must be specified in the transkit.xml file. The default "Access Modifier" for the RESX resources is "Internal". This "Access Modifier" MUST be changed to "Public" to make the string resources accessible to XAML.

Note
The Access Modifier for RESX files in WPF AddIns MUST be set to "Public" for XAML access.

XAML File

String resources are referenced in XAML files using the x:Static markup extension.

. . .
xmlns:props="clr-namespace:WPFSample.Properties"
. . .
<TextBlock Text="{x:Static props:Resources.Hello}" />

C# File

String resources are referenced in C# files using the Properties.Resources class located in the generated *.Designer.cs files based on entries in the RESX files.

. . .
using WPFSample.Properties;
. . .
string.Format ("{0}!", Resources.Hello);

Attaching WPF Content to PowerPlatform

WPF Windows or UserControls can be attached to PowerPlatform using the following classes in the Bentley.MstnPlatformNET.WPF namespace.

WPFInteropHelper

WPFInteropHelper allows floating/non-dockable WPF Window instances to be integrated into the PowerPlatform as "First class citizens". That means that focus, the window list, minimize, restore, function keys, etc. are all handled appropriately within PowerPlatform.

using System;
using System.Windows;
using Bentley.MstnPlatformNET.WPF;
namespace WPFSample.Views
{
public partial class FloatWindow : Window
{
private static FloatWindow s_window;
private WPFInteropHelper m_wndHelper;
public FloatWindow ()
{
InitializeComponent ();
// Create the ViewModel and set the DataContext
var viewModel = new ViewModels.FloatWindowViewModel ();
this.DataContext = viewModel;
// Create the PowerPlatform Interop Helper and Attach the Window
m_wndHelper = new WPFInteropHelper (this);
m_wndHelper.Attach (WPFSampleApp.Instance, true, "WPFSampleFloat");
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
protected override void OnClosed (EventArgs e)
{
base.OnClosed (e);
m_wndHelper.Detach ();
m_wndHelper.Dispose ();
s_window = null;
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
public static void OpenWindow ()
{
if (null == s_window)
{
s_window = new FloatWindow ();
s_window.Show ();
}
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
public static void CloseWindow ()
{
if (null != s_window)
{
s_window.Close ();
}
}
}
}

DockableWindow

DockableWindow allows a UserControl to be placed in a dockable window. Note that there is no XAML for a DockableWindow itself; the WPF content comes from a UserControl.

using System;
using SD = System.Drawing;
using Bentley.MstnPlatformNET.WPF;
namespace WPFSample.Views
{
class MyDockWindow : DockableWindow
{
static private MyDockWindow s_Window;
public MyDockWindow ()
{
var userControl = new UserControl1 ();
this.Content = userControl;
// Optional ContentCloseQuery event handler
//this.ContentCloseQuery += new Bentley.Windowing.ContentCloseEventHandler (ContentCloseQueryHandler);
this.Title = "Dockable Window";
this.Attach (WPFSampleApp.Instance, "WPFSampleDock", new SD.Size (100, 100));
userControl.TestImageUpdate ();
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
protected override void OnClosed (EventArgs e)
{
base.OnClosed (e);
this.Detach ();
s_Window = null;
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
void ContentCloseQueryHandler (object sender, Bentley.Windowing.ContentCloseEventArgs e)
{
//System.Diagnostics.Debug.WriteLine (string.Format (
// "Bentley.Windowing.ContentCloseQuery - closeAction {0}, userAction {1}", e.CloseAction, e.UserAction));
}
protected override void OnClosing (System.ComponentModel.CancelEventArgs e)
{
base.OnClosing (e);
}
protected override void OnActivated (EventArgs e)
{
base.OnActivated (e);
}
protected override void OnDeactivated (EventArgs e)
{
base.OnDeactivated (e);
}
protected override void OnDocking (DockEventArgs e)
{
base.OnDocking (e);
}
protected override void OnDocked (DockEventArgs e)
{
base.OnDocked (e);
}
protected override void OnUndocking (EventArgs e)
{
base.OnUndocking (e);
}
protected override void OnUndocked (EventArgs e)
{
base.OnUndocking (e);
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
public static void OpenWindow ()
{
if (null == s_Window)
{
s_Window = new MyDockWindow ();
s_Window.Show ();
}
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
public static void CloseWindow ()
{
if (null != s_Window)
{
s_Window.Close ();
}
}
}
}

DockableToolbar

DockableToolbar allows a UserControl to be placed in a dockable toolbar or small dialog. Note that there is no XAML for a DockableToolbar itself; the WPF content comes from a UserControl.

using System;
using SD = System.Drawing;
using SW = System.Windows;
using Bentley.MstnPlatformNET.WPF;
using BMG = Bentley.MstnPlatformNET.GUI;
namespace WPFDemo
{
class MyToolbar : DockableToolbar, BMG.IGuiDockable
{
static private MyToolbar s_MyToolbar;
public MyToolbar ()
{
var toolbarControl = new ToolbarControl ();
toolbarControl.VerticalContentAlignment = SW.VerticalAlignment.Center;
this.Content = toolbarControl;
this.Title = "Dockable Toolbar";
this.AttachingToHost += new BMG.AttachingToHostEventHandler (MyToolbar_AttachingToHost);
this.DetachingFromHost += new EventHandler (MyToolbar_DetachingFromHost);
this.Attach (WPFDemoApp.Instance, "WPFDemo5");
// Setup AutoOpen after calling Attach()
this.AutoOpen = true;
this.AutoOpenKeyin = "mdl silentload wpfdemo,,DEFAULTDOMAIN;wpfdemo open5";
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
protected override void OnClosed (EventArgs e)
{
base.OnClosed (e);
this.Detach ();
this.Dispose ();
s_MyToolbar = null;
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
public static void OpenWindow ()
{
if (null == s_MyToolbar)
{
s_MyToolbar = new MyToolbar ();
s_MyToolbar.Show ();
}
}
//------------------------------------------------------------------------------------
//--------------+---------------+---------------+---------------+---------------+------
public static void CloseWindow ()
{
if (null != s_MyToolbar)
{
s_MyToolbar.Close ();
}
}
#region IGuiDockable Members
private SD.Size m_rejectedSize = SD.Size.Empty;
public bool GetDockedExtent (BMG.GuiDockPosition dockPosition, ref BMG.GuiDockExtent extentFlag, ref SD.Size dockExtent)
{
dockExtent.Height = this.CommonDockSize.Height;
if (dockPosition == BMG.GuiDockPosition.Top ||
dockPosition == BMG.GuiDockPosition.Bottom)
{
dockExtent.Width = (int) this.ActualWidth;
extentFlag = BMG.GuiDockExtent.Specified;
}
else if (dockPosition == BMG.GuiDockPosition.NotDocked)
extentFlag = BMG.GuiDockExtent.Specified;
else
extentFlag = BMG.GuiDockExtent.InvalidRegion;
return true;
}
public bool WindowMoving (BMG.WindowMovingCorner corners, ref SD.Size newSize)
{
newSize.Height = CommonDockSize.Height;
if (corners != BMG.WindowMovingCorner.LowerRight || m_rejectedSize.Equals (newSize))
{
m_rejectedSize = newSize;
newSize.Width = (int) this.ActualWidth;
}
return true;
}
#endregion
void MyToolbar_AttachingToHost (object sender, BMG.AttachingToHostEventArgs e)
{
e.AttachPoint = new SD.Point (0, 0);
//e.Handled = true;
}
void MyToolbar_DetachingFromHost (object sender, EventArgs e)
{
}
}
}

ToolSettingsHost

ToolSettingsHost allows a UserControl to be placed into the Tool Settings Window when a tool is started. Currently, the Bentley.Interop.MicroStationDGN.IPrimitiveCommandEvents is subclassed to create a managed tool. But a DgnPrimitiveTool class will be available in "DgnDisplayNET" soon. An example will be provided at that time.

ViewOverlay / ViewWidget

The ViewOverlay and ViewWidget classes provide a mechanism of overlaying a WPF window over a PowerPlatform View window as an overlay. The View Overlay may contain controls, which are then treated as View Widgets. When the widgets are not active, they are drawn into the View using QuickVision. These classes are still in development. When the work is completed, an example will be provided at that time.

RibbonBar Support

Support for a RibbonBar interface has been added to PowerPlatform for the CONNECT Edition release. When using the RibbonBar interface, the main window is switched to a Telerik WPF RadRibbonWindow, and is provided a RadRibbonView by the "RibbonView" ClrApp. The RadRibbonView object may be obtained by any application using the Bentley.MstnPlatformNET.WPF.RibbonBar class, and applications may add their own tabs, groups and controls to the Ribbon.

MstnPlatformNET.Commands

Many controls in the Ribbon will invoke native PowerPlatform functionality. PowerPlatform commands and keyins are exposed to .NET managed code via classes in the MstnPlatformNET.Commands namespace in MicroStation.dll.

Command Example - C++/CLI

The following is an example of a PowerPlatform command number that is exposed to .NET managed code in the MstnPlatformNET.Commands namespace. In this example, the CMD_CREATE_DRAWING command number is wrapped by the CreateDrawing class. The Feature Aspect checked for this command is BP::AspectID::File_New.

public ref class CreateDrawing : public CommandNumberMstnCommand
{
public: CreateDrawing () : CommandNumberMstnCommand (CMD_CREATE_DRAWING) {}
protected: virtual property BP::AspectID FeatureAspectID
{
BP::AspectID get() override
{
return BP::AspectID::File_New; // @fadoc CreateDrawing Command
}
}
};

Copyright © 2017 Bentley Systems, Incorporated. All rights reserved.