Thursday, May 19, 2011

Run WPF Applications on Linux

One of the recuring questions the mono team get is will they support WPF in mono. As Miguel de Icaza posted it his blog this would take years or work to implement the entire WPF framework over to mono. There is a quick alternative if you dont mind not having the full WPF framework and opting for Silverlight 3/4 compatability for your xaml and some of the framework. While you can produce Desktop applications using Silverlight and Moonlgiht with the new Out of Browser option , this kind of applicaiton still runs in a sandboxed environment which is one of the reasons people choose WPF over Silverlight in some cases because it gives you full access to the .net framework ( for example UDP Sockets, Hard disk access etc)

One of my more recent c# projects was producing a skinable dekstop user interface for a project, the obvious solution would be use WPF. The style support is built in and fairly easy to use so it was a no brainer, the only down side was that it needed to run on an embedded system running linux. Enter Moonlight... the Moonlight project does provide a way to run apps on the desktop using a program called mopen. This program creates a GTK window in which to host the moonlight runtime engine, the main drawback is that it only supports running the xaml files directly or loading a .xap file.

One of the other requirements was that the system needed to support plugin dlls for new screens, while allot of this could be accomplished using xap files, I decided to take a look a what it would take to write a true desktop moonlight app which would run from a mono application.

In order for this whole system to work a couple of changes were needed to the MoonlightHost.cs and the Deployment.cs classes in the moonlight source code. My previous blog post showed how to patch the files and provides a link to a ubuntu repository which you can use (for 10.04) to test this stuff out.

The whole applicaiton is based around a "host" application like mopen, but speficially designed to handle loading xaml based screens from the local directory rather than from an extracted xap file. Here is a class layout of the project





















Our app entry point is  the main static method in Program.cs. Within this method we will use the WindowManager to Create an IWindow instance. As you probably guessed under windows we will create a WPFWindow instance, but under moonlight we create a MoonlightWindow. Both of these window classes support the IWindow interface and as such have a Run method and a Content property.

The Run method does what you'd expect, it shows the window and does not return until the window is closed. The Content property is of type System.Windows.UIElement and this is where we set the control we want to display. For now this is hardcoded but we will add support for loading the content from a dll later.

Lets look at the contents of the WPFWindow class.

public class WPFWindow : IWindow
 {
     private System.Windows.Window window;
     private System.Windows.Application app = new System.Windows.Application();  
  
     public WPFWindow(string[] args)
     {      
        window = new System.Windows.Window();    
        window.SizeToContent = SizeToContent.WidthAndHeight;
        window.Background = new SolidColorBrush(Colors.Black);
        window.Closed += delegate 
        {          
           Dispatcher.CurrentDispatcher.InvokeShutdown();
        };                   
        window.UpdateLayout();
     }
               
     public void Exit()
     {
      window.Close();
     }
          
     public void Run()
     {
      app.Run(window);
     }
     
     public UIElement Content
     {
      get {
          return (UIElement)window.Content;
      }
      set {
          window.Content = value;
      }
     }
 }
All fairly straight forward. We manually create a WPF Window and a System.Windows.Application (which is a WPF host).

So lets take a look at the Moonlight Window

public class MoonlightWindow : IWindow
 {
 
     private Gtk.Window window;
     private MoonlightHost host;
      
     public MoonlightWindow(string[] args)
     {
  string manifest = BuildAppManifest();
  System.IO.File.WriteAllText("appmanifest.xaml", manifest); 
AppDomain.CurrentDomain.AssemblyResolve += delegate (object sender, ResolveEventArgs rea) 
         {
    string mapped = MapVersion (rea.Name);
    Assembly result = mapped != null ? Assembly.Load (mapped) : null;
    if(result == null)
    {
       Debug.WriteLine("AssemblyResolveEvent ({0}, {1}) mapped to: '{2}' loaded assembly: '{3}'", sender, rea.Name, mapped, result != null ? result.FullName : "not found");
    }      
         return result;
         };
   
         Gtk.Application.Init();
                MoonlightRuntime.Init();

                window = new Gtk.Window("");
  window.DeleteEvent += delegate
                {                 
                  Gtk.Application.Quit();
                };              
            
  host = new MoonlightHost();   
                host.LoadDesklet();                 
                window.Add(host);       
    }
            
           public void Exit()
           {          
              Gtk.Application.Quit();
           }
  
    public void Run()
    {
  window.ShowAll();            
  Gtk.Application.Run();
      }
  
    public UIElement Content
    {
      get {
       return host.Application.RootVisual;
      }
      set {
       host.Application.RootVisual = value; 
FrameworkElement top = (FrameworkElement)value; 
host.SetSizeRequest((int)top.Width, (int)top.Height); 
window.Resize((int)top.Width, (int)top.Height); 
}
    }
  
  
  /// 
  /// Builds the appmanifest file for Moonlight. This is required so that the correct styles and classes are initialized 
  /// 
  /// 
  internal string BuildAppManifest()
  {
   
   StringWriter sw = new StringWriter();
sw.WriteLine("<Deployment xmlns='http://schemas.microsoft.com/client/2007/deployment' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' ");
   sw.WriteLine(" RuntimeVersion='4.0.40624.0'>");
   sw.WriteLine("<Deployment.Parts>");
      
   string[] screenfiles = Directory.GetFiles(".", "*.dll");
   foreach(string file in screenfiles)
   {
    if (!file.StartsWith("Moonlight.") && !file.StartsWith("System."))
    {
       sw.WriteLine(String.Format("<AssemblyPart x:Name='{0}' Source='{1}'/>",Path.GetFileNameWithoutExtension(file), file));
    }
   } 
   sw.WriteLine("</Deployment.Parts>");
   sw.WriteLine("</Deployment>");     
   return sw.ToString();
   
  }
  
  static string MapVersion (string name)
  {
   string version = typeof (int).Assembly.GetName ().Version.ToString ();
   switch (name) {
    case "Microsoft.VisualBasic, Version=2.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35":
     return "Microsoft.VisualBasic, Version=" + version + ", Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
    case "mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e":
     return "mscorlib, Version=" + version + ", Culture=neutral, PublicKeyToken=b77a5c561934e089";
    case "System.Core, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e":
     return "System.Core, Version=" + version + ", Culture=neutral, PublicKeyToken=b77a5c561934e089";
    case "System, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e":
     return "System, Version=" + version + ", Culture=neutral, PublicKeyToken=b77a5c561934e089";
    case "System.Net, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e":
     return "System.Net, Version=" + version + ", Culture=neutral, PublicKeyToken=7cec85d7bea7798e";
    case "System.Runtime.Serialization, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e":
     return "System.Runtime.Serialization, Version=" + version + ", Culture=neutral, PublicKeyToken=b77a5c561934e089";
    case "System.ServiceModel, Version=2.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35":
     return "System.ServiceModel, Version=" + version + ", Culture=neutral, PublicKeyToken=b77a5c561934e089";
    case "System.ServiceModel.Web, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e":
     return "System.ServiceModel.Web, Version=" + version + ", Culture=neutral, PublicKeyToken=31bf3856ad364e35";
    case "System.Xml, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e":
     return "System.Xml, Version=" + version + ", Culture=neutral, PublicKeyToken=b77a5c561934e089";
 
    /* these are the only 3.0 assemblies we need to redirect to, all the others redirect to fx assemblies */
    case "System.Windows.Browser, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e":
     return "System.Windows.Browser, Version=3.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756";
    case "System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e":
     return "System.Windows, Version=3.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756";
   }
 
   return null;
  }
 
    }

This is a bit more complicated. There are some bit that have been borrowed from mopen. MapVersion makes sure that the Moonlight/Silverlight dll references are mapped to the correct Moonlight Versions. BuildAppManifest is a method that builds an appmanifest.xaml file when the application starts, this is required so that Moonlight correct loads the default themes for the components and also to ensure that any external dll's that contain xaml resources or components are registered with Moonlight.

The most important lines are the following

host = new MoonlightHost();   
host.LoadDesklet();    

This is where the new method on the MoonlightHost class , LoadDesklet, is called. This method initializes the MoonlightRuntime so that it will load assemblies from the local application directory and not from a temp directory where a xap file was extracted to.

So with all of those pieces in place lets take a look at the main method in Program.cs

public class Program
 {
  [STAThread()]
  public static void Main(string[] args)
  {
   IWindow window = WindowManager.CreateWindow(args);
   
   var c = new Canvas();
   c.Width = 640;
   c.Height = 480;
   c.Background = new SolidColorBrush(Colors.Blue);
   window.Content = c;
      
   window.Run();
  }
 }

The is really simple code. We create the IWindow using the WindowManager, then create our content and assign that to the window.Content property and finally run the app. This will produce the same result in Windows and Linux.

Building the application under windows, you can just open the solution with Visual Studio 2010 (Express) or greater or use the SharpDevlop 4.0 IDE which is a great tool.

Under linux this soltuion might build under monodevelop, but I havent tested it. I prefer to use xbuild from the command prompt. In order to build the project you must first have installed a patched version of mono and moonlight with the new methods. I have a debian package available which you can read about how to install here.

To use these package you will need to use the source command to update the search paths. You can download the appropriate script from here, then use
source /usr/local/bin/moon4-dev-env

once you are in a parallel enviroment (you will see a [moon4-dev] at the console) you can run the following commands to build and then run the app.

xbuild  WPFMoonlight.sln
cd ./WPFMoonlight/bin/Debug
mono WPFMoonlight.exe

This should run up the app. Currently the content is hard coded but this can be replaced with calls to Assembly.LoadFile or LoadFrom to dynamiacally load assemblies and then use Reflection to figure out which Content to show.

I'll try to post something up that shows how to do that via a nice plugin interface. In the meantime you can get the source code for this project here.

No comments:

Post a Comment