WPF and Powershell: Library-PresentationInterface(Part 1)

In the past, when it came to generating a GUI using Powershell, the only option was to use System.Windows.Forms.dll to generate the GUI. This method required lots of verbose script, and the result was not always pretty. The PresentationFramework.dll library provided the XAML loader, but the Presentation classes required an STA thread to initialize while the threads that interpreted Powershell scripts are always MTA.

I really wanted to be able to create WPF windows in Powershell so I created a library script that will define several classes that will handle the initialization of an STA thread that can then handle the initialization of a WPF window.

Here are a list of a few of the major features of the library script.

  • Windows can be initialized from XAML.
  • Windows can be initialized using the default constructor of a class that derives from the window type.
  • The Tag property can be set prior to initialization. Arguments for the window can be passed here.
  • The value of the Tag property is returned when the window is closed. This can be useful in the case that a bool dialog result is not enough.
  • The window threads operate asynchronously to Powershell. Synchronization is done useing an IAsyncResult object.
  • The Dispatch object that is associated with the window’s UI thread is attached to the IAsyncResult object. External threads cannot interact with user interface elements, but can use the Dispatch object to give instructions to the UI thread.

Later I will be giving four examples of how to use this library.

  1. The first will give a simple example of the use XAML.
  2. The second will be an example of using custom types with XAML.
  3. The third will be an example of using a compiled window type.
  4. The forth example will show how to use the Dispatch object.

To use this library script you will first need to save a copy of the New-CAssembly script either as a function or as a script file in a path location.

Use of the library is fairly simple. Simply dot source the library. Then, call the Start-PresentationInterface function passing either the XAML as an XML node to the Xaml parameter, or the type object of any compiled window type to the WindowType parameter. You may also initialize the window’s Tag property by passing it to the WindowTag parameter. The result of that function will give an IAsyncResult object. It behaves just like any other IAsyncResult object, and has a IExposeDispatch interface giving it the Dispatch property. That object may be disposed of safely or you can pass it to the Result parameter of the Stop-PresentationInterface function. That function will block the calling thread untill the window has closed. The return value will give the DialogResult of the window and the final value of the Tag property of the window.

The script for the library is below, but first here is a really simple example to get you started.

. Library-PresentationInterface
# Initialize a window
$result = Start-PresentationInterface -xaml (
	[xml]'<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"><Viewbox><TextBlock>Welcome to the Presentation Framework!</TextBlock></Viewbox></Window>'
)
# Look at the result object
$result
# Wait for the window to close
Stop-PresentationInterface $result

Now the script.

# Library-PresentationInterface.ps1
# Version: 1.0
# Author: LunaticExperimentalist
# End User License: Public Domain/Unrestricted Use
# Provided "as is," with out warranty, and without any assertions of suitability
# for any purpose either express or implied. 

if ($args -contains '-?') { @'
Library-PresentationInterface
VERSION: 1.0
AUTHOR: LunaticExperimentalist

USAGE: Library-PresentationInterface

DESCRIPTION: This library contains two functions and four types. The types are
  only used by the functions and do not need to used directly. The two
  functions, Start-PresentationInterface and Stop-PresentationInterface, provide
  access to the Windows Presentation Framework. 

CONTAINS: Start-PresentationInterface
          Stop-PresentationInterface
'@
return
}


[Void]([Reflection.Assembly]::LoadWithPartialName('PresentationFramework'))
[Void](New-CAssembly @"
using System;
using System.Threading;
using System.Windows;

namespace LibraryPresentationInterface {
	// structure returned by the stop method
	public struct PresentationResult {
		// Contains the value of the DialogResult property of the window
		public bool DialogResult;
		// Contains the value of the Tag property of the window
		// may be used if a bool is insufficient
		public object WindowTag;
		public PresentationResult(bool dialogResult, object tag) {
			DialogResult = dialogResult; WindowTag = tag;
		}
	}

	// public interface can give a reference to the dispatcher assosiated with a window
	public interface IExposeDispatcher {
		System.Windows.Threading.Dispatcher Dispatcher {get;}
	}
	
	// this is an IAsynResult object returned by the start method
	// may be passed to the stop method to retrieve the dialog result or
	// may be discarded safely
	internal class PresentationAsyncResultImpl : IAsyncResult, IExposeDispatcher {
		object UserObject;
		EventWaitHandle Handle;
		bool Completed;
		PresentationResult DialogResult;
		bool IsError;
		System.Exception Error;
		System.Windows.Threading.Dispatcher _Dispatcher;
		
		public PresentationAsyncResultImpl(object userObject, EventWaitHandle handle) {
			UserObject = userObject; this.Handle = handle; Completed = false;
		}
		// returns the user defined object that was given at the start method
		public object AsyncState {get{return this.UserObject;}}
		// returns a wait handle that will be signaled when the window is closed
		public WaitHandle AsyncWaitHandle {get{return this.Handle;}}
		// returns false, this is an asyncronous method
		public bool CompletedSynchronously {get{return false;}}
		// returns true if the window is closed, or if initialization has failed
		public bool IsCompleted {get{return this.Completed;}}
		// returns the dispatcher of the UI thread of the window
		public System.Windows.Threading.Dispatcher Dispatcher {get {return _Dispatcher;}}
		// used by the stop mehtod to get the result
		internal PresentationResult Result {get{
			if (IsError)
				throw Error;
			return DialogResult;
		}}
		// used to set the result when the window is closed
		internal void SetComplete(PresentationResult result) {
			this.DialogResult = result; this.Completed = true; this.Handle.Set();
		}
		// used to set the dispatcher once the window is initialized
		internal void SetDispatcher(System.Windows.Threading.Dispatcher dispatcher) {
			_Dispatcher = dispatcher;
		}
		// used to set the result to throw an exception
		internal void SetException(System.Exception ex) {
			this.IsError = true; this.Error = ex; this.Completed = true;
			this.Handle.Set();
		}
	}
	
	// Args passed to the window thread
	internal class WindowArgs {
		// [xml]containing the xaml for the window or a [type] for a self describing window type
		public object WindowInfo;
		// initial value for the window's tag
		public object WindowTag;
		// an internal reference to the IAsyncResult object so the the result can be set
		public PresentationAsyncResultImpl AsyncResult;
		// initialization wait handle, signaled once initialization is completed
		public EventWaitHandle InitHandle;
		// an initialization exception that, if set, will be thown by the start method
		public Exception InitException; 
	}
	
	public class Presentation {
		// internal method to start the window thread
		private static IAsyncResult InternalStart (object windowInfo, object tag, object userObject) {
			WindowArgs Args = new WindowArgs();
			Args.WindowInfo = windowInfo; Args.WindowTag = tag;
			Args.AsyncResult = new PresentationAsyncResultImpl(userObject, new EventWaitHandle(false, EventResetMode.ManualReset));
			Args.InitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
			Args.InitException = null;
			Thread WindowThread = new Thread(new ParameterizedThreadStart(WindowProc));
			WindowThread.Name = "Presentation.Thread";
			WindowThread.SetApartmentState(ApartmentState.STA);
			WindowThread.Start(Args);
			Args.InitHandle.WaitOne();
			if (Args.InitException != null)
				throw Args.InitException;
			return Args.AsyncResult;
		}
		// public start method to accept xaml
		public static IAsyncResult Start(System.Xml.XmlNode xaml, object tag, object userObject) {
			if (xaml == null)
				throw new ArgumentNullException("xaml");
			return InternalStart(xaml, tag, userObject);
		}
		// public start method to accept a window type
		public static IAsyncResult Start(Type windowClass, object tag, object userObject) {
			if (windowClass == null)
				throw new ArgumentNullException("windowClass");
			return InternalStart(windowClass, tag, userObject);
		}
		// stop method to retrieve the dialog result 
		public static PresentationResult Stop(IAsyncResult asyncResult) {
			if (asyncResult == null)
				throw new ArgumentNullException("asyncResult");
			if (!(asyncResult is PresentationAsyncResultImpl))
				throw new ArgumentException("Unrecognised result type.");
			PresentationAsyncResultImpl Result = (PresentationAsyncResultImpl)asyncResult;
			Result.AsyncWaitHandle.WaitOne();
			return Result.Result;
		}
		// procedure for the window thread
		private static void WindowProc(object obj) {
			WindowArgs Args = (WindowArgs)obj;
			Window LocalWindow;
			PresentationResult FinalResult = new PresentationResult();
			// trap initialization errors
			try {
				if (Args.WindowInfo is System.Xml.XmlNode) {
					// initialize from xaml
					object XamlObject = System.Windows.Markup.XamlReader.Load(
						new System.Xml.XmlNodeReader((System.Xml.XmlNode)Args.WindowInfo));
					if (!(XamlObject is Window))
						throw new InvalidCastException("The given XAML did not return a Window object.");
					LocalWindow = (Window)XamlObject;
				}
				else {
					// initialize from type
					Type WindowType = (Type)Args.WindowInfo;
					System.Reflection.ConstructorInfo Constructor = WindowType.GetConstructor(Type.EmptyTypes);
					if (Constructor != null) {
						LocalWindow = (Window)Constructor.Invoke(new Object[0]);
					}
					else {
						throw new ArgumentException(String.Format("The type"{0}" does not have an accessable default constructor.",WindowType.FullName));
					}
				}
				LocalWindow.Tag = Args.WindowTag;
			}
			catch (Exception ex) {
				// set initialization exception
				Args.InitException = ex;
				Args.InitHandle.Set();
				return;
			}
			// initialization complete
			// set the dispatcher
			Args.AsyncResult.SetDispatcher(LocalWindow.Dispatcher);
			// set the initialization wait handle
			Args.InitHandle.Set();
			// trap runtime exceptions
			try {
				FinalResult.DialogResult = (bool)LocalWindow.ShowDialog();
				FinalResult.WindowTag = LocalWindow.Tag;
			}
			catch (Exception ex) {
				// set the runtime exception to be rethrown by the stop method
				Args.AsyncResult.SetException(ex);
				return;
			}
			// set the dialog result
			Args.AsyncResult.SetComplete(FinalResult);
		}
	}
}
"@)

function Start-PresentationInterface([System.Xml.XmlNode]$Xaml, [Type]$WindowType, $WindowTag, $UserObject) {
if (($args -contains '-?') -or ((!$Xaml) -and (!$WindowType)) -or (($Xaml) -and ($WindowType))) { @'
Library-PresentationInterface
VERSION: 1.0
AUTHOR: LunaticExperimentalist

USAGE:
  Start-PresentationInterface -Xaml [System.Xml.XmlNode]
	  [-WindowTag [Object]] [-UserObject [Object]]
  Start-PresentationInterface -WindowType [Type] [-WindowTag [Object]]
	  [-UserObject [Object]]

PARAMETERS:
  Xaml: An XML node, such as an XML document, in the format of XAML that
    describes the content of a Presentation Framework window. This argument is
    mandatory and mutually exclusive to WindowType.

  WindowType: May be any type derived from and including the type
    System.Windows.Window. The type must have a public default constructor.
    The default constuctor of this type is called and the resulting window
    object is displayed. This argument is mandatory and mutually exclusive to
    Xaml.
    
  WindowTag: The value of this argument is applied to the Tag property of the
    window after initialization.
  
  UserObject: This object is not used internally. It is only made available
    though the resulting IAsyncResult object.

RETURNS: An IAsyncResult object that can be used to get the dialog result of
  the window.

DESCRIPTION: Initializes a new window using the information provided by the
  WindowType or Xaml arguments.

SEE ALSO: Stop-PresentationInterface
'@
return
}
	if ($Xaml) {
		[LibraryPresentationInterface.Presentation]::Start($Xaml,$WindowTag,$UserObject)
	}
	else {
		[LibraryPresentationInterface.Presentation]::Start($WindowType,$WindowTag,$UserObject)
	}
}

function Stop-PresentationInterface([IAsyncResult]$Result) {
if (($args -contains '-?') -or (!$Result)) { @'
Stop-PresentationInterface
VERSION: 1.0
AUTHOR: LunaticExperimentalist

USAGE:
  Stop-PresentationInterface -Result [IAsyncResult]

PARAMETERS:
  Result: An IAsyncResult object obtained from the Start-PresentationInterface
    function.
	
RETURNS: An object of type LibraryPresentationInterface.PresentationResult
  containing dialog result information.

DESCRIPTION: Causes the current thread to wait for the window corresponding the
  given IAsyncResult object to close.

SEE ALSO: Start-PresentationInterface
'@
return
}
	[LibraryPresentationInterface.Presentation]::Stop($Result)
}
Advertisements

~ by lunaticexperimentalist on November 8, 2007.

3 Responses to “WPF and Powershell: Library-PresentationInterface(Part 1)”

  1. OK, you’ve convinced me, you ARE a lunatic…better you than me!Anyway, thanks for sharing your lunacy. Your New-CAssembly script has a few occurrences of ‘new’ that need to be ‘New-Object’ — perhaps they worked in earlier versions of PS?  Changing that made this work fine.

  2. Thank you for catching that! I don’t believe that any version of Powershell had anything defined as "new." I have new aliased to New-Object. I tried to remove all instances of any aliases, but failed.Again, thanks for catching what is basically a typo. I’ve fixed the post and now the script should work for new-less Powershell users.

  3. Thanks for the advice.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: