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.

Tuesday, May 17, 2011

Using Moonlight for Desktop Applications

One of the things people seem to ask is if you can get WPF applications working under linux. While you cant take a WPF app and run it directly there are ways to utilize the Moonlight engine to resuse your business logic and the xaml resources to work in a desktop senario under linux.

Currently Moonlight has a tool called mopen which allows you to host xap files in a desktop widget which is a nice way to package your entire application up and get it to run on the desktop. There is a tool called mpack which you can use to build the desktop xap file to use with mopen. However if you want to just have an exe and load the dll's locally then mopen is not going to cut it..yet, there are comments in the code which suggest this might be added in the future.

In order to develop full desktop applications with moonlight you will need to patch the current moonlight trunk with the patch at the end of this entry. I wont go into the details on how to get moonlight building as there are notes on how to do this in the README files on git.

If you would rather not have to build the source yourself I have a public repository for ubuntu distros which contains both the runtime only environment (no compilers or debug support) and the development environment, these debians are based on the latest mono trunk (2.11) and the latest moonlight 4 trunk.

The runtime debian is about 30MB, the development one is 97MB. Note there are allot (and I mean allot) of dependencies for moonlight so it might take a while.

To use the repository you will need to add
deb http://repo.infinitespace-online.net /
to your sources.list (via the GUI or via the command line) then run
sudo apt-get update
sudo apt-get install moon4-desktop
for the runtime or
sudo apt-get update
sudo apt-get install moon4-desktop-dev
for the development environment.

Both debians install into the /opt/ folder rather than in /usr. This is so you dont mess up any existing mono installation. However you will need to use the "source" command to update your environment do use the new mono install. Both debians place a script in /usr/local/bin so you can run the following commands

source /usr/local/bin/moon4-env

for the runtime environment and

source /usr/loca/bin/moon4-dev-env

for the development environment. In both cases your terminal should change from your usual @ to a [moon4] or [moon4-dev].

Now if you want to build your own version of the desktop support then you will need to get the latest moon trunk from git and apply the following patch.

diff --git a/class/System.Windows/System.Windows/Deployment.cs b/class/System.Windows/System.Windows/Deployment.cs
index 9962983..ea3c94c 100644
--- a/class/System.Windows/System.Windows/Deployment.cs
+++ b/class/System.Windows/System.Windows/Deployment.cs
@@ -299,11 +299,28 @@ namespace System.Windows {
     throw new MoonException (2105, e.Message);
    }
   }
+  
+  internal bool InitializeDeployment (string localPath) {
+   XapDir = localPath;   
+   TerminateAndSetCulture (null, null);
+
+   NativeMethods.deployment_set_initialization (native, true);
+   try {
+    EntryPointType = "System.Windows.Application";
+    EntryPointAssembly = typeof (Application).Assembly.GetName ().Name;
+    EntryAssembly = typeof (Application).Assembly;
+    ReadManifest ();
+    return LoadAssemblies ();
+   }
+   finally {
+    NativeMethods.deployment_set_initialization (native, false);
+   }
+  }
 
   internal bool InitializeDeployment (string culture, string uiCulture)
   {
    TerminateAndSetCulture (culture, uiCulture);
-
+            
    NativeMethods.deployment_set_initialization (native, true);
    try {
     EntryPointType = "System.Windows.Application";
diff --git a/gtk/Moonlight.Gtk/MoonlightHost.cs b/gtk/Moonlight.Gtk/MoonlightHost.cs
index b2a17e9..a085b13 100644
--- a/gtk/Moonlight.Gtk/MoonlightHost.cs
+++ b/gtk/Moonlight.Gtk/MoonlightHost.cs
@@ -70,10 +70,10 @@ namespace Moonlight.Gtk
    Mono.Xaml.XamlLoader.AllowMultipleSurfacesPerDomain = true;
 
    windowingSystem = NativeMethods.runtime_get_windowing_system ();
-   window = NativeMethods.moon_windowing_system_create_window (windowingSystem, MoonWindowType.Plugin, 0, 0, IntPtr.Zero, IntPtr.Zero);
+   window = NativeMethods.moon_windowing_system_create_window (windowingSystem, MoonWindowType.Plugin, 0, 0, IntPtr.Zero, IntPtr.Zero);        
    surface = NativeMethods.surface_new (window);
    Raw = NativeMethods.moon_window_gtk_get_native_widget (window);
-
+   
    SizeAllocated += OnSizeAllocated;
   }
 
@@ -122,6 +122,17 @@ namespace Moonlight.Gtk
   }
 
   /// 
+  /// Initializes the Moonlight Plugin for a Desktop environment
+  /// This will allow moonlight data to be loaded from other dll's and not from within a xap file.
+  /// 
+  public void LoadDesklet()
+  {
+   string path = System.IO.Path.GetDirectoryName(".");
+      
+   Deployment.Current.InitializeDeployment(path);
+  }
+  
+  /// 
   ///    Initializes the Surface widget from the XAML contents in a string
   /// 
   /// The contents of the string.@@ -146,6 +157,7 @@ namespace Moonlight.Gtk
    Content = (FrameworkElement)toplevel;
   }
 
+
   /// 
   ///    Initializes the GtkSilver widget from the XAML contents in a file
   /// 
diff --git a/src/bitmapimage.cpp b/src/bitmapimage.cpp
index 0d1c901..f183d66 100644
--- a/src/bitmapimage.cpp
+++ b/src/bitmapimage.cpp
@@ -502,7 +502,7 @@ BitmapImage::CreateLoader (unsigned char *buffer)
    moon_error = new MoonError (MoonError::EXCEPTION, 4001, "unsupported image type");
   }
  } else {
-  loader = Runtime::GetWindowingSystem ()->CreatePixbufLoader (NULL);
+  loader = Runtime::GetWindowingSystem ()->CreatePixbufLoader ("png");
  }
 }
 
diff --git a/src/deployment.cpp b/src/deployment.cpp
index 14e55fa..3fe738f 100644
--- a/src/deployment.cpp
+++ b/src/deployment.cpp
@@ -205,6 +205,7 @@ Deployment::Initialize (const char *platform_dir, bool create_root_domain)
   Deployment::desktop_deployment->InitializeDesktop (root_domain);
   Deployment::SetCurrent (Deployment::desktop_deployment);
   Deployment::desktop_deployment->EnsureManagedPeer ();
+                Deployment::desktop_deployment->InitializeAppDomain();
 
   Application *desktop_app = MoonUnmanagedFactory::CreateApplication ();
   desktop_deployment->SetCurrentApplication (desktop_app);


then build and install.

The Patch adds a new method (amongst other things) to the Moonlight.Gtk.MoonlightHost class called LoadDesklet which initializes the moonlight engine to allow local dll's to be loaded rather than expecting the binaries to be packed up in a xap file. This is more like the way WPF works under windows. Although you are still restricted to Silverlight 3/4 based Xaml support.

Next time I'll go through how to use this new stuff to develop desktop applications.