Silverlight, Out-Of-Browser and Multiple Windows

I was talking to a developer the other day about Silverlight out-of-browser applications and how there’s not really any support for opening multiple windows so that (e.g.) you might have a master window in the application and the ability to open sub-windows.

It’s a feature that’s often requested and I think it’s referred to from this request here although that actually asks for MDI which I think you could achieve a form of with Silverlight’s ChildWindow class.

I was wondering how this might be done and got thinking about how it might be possible to have a scheme whereby;

  1. Your main Silverlight application runs.
  2. The user wants to open a new window in the application.
  3. You run a second instance of the application.
  4. The second instance of the application is told to navigate to some content that makes it the second window rather than the primary window.

This is mostly an experimentation rather than something I’d advise, I just wanted to see how it might work.

In The Browser

In the browser, I don’t think this is too difficult and there’s no doubt a number of ways you can do it but you can use HtmlPage.PopupWindow in order to open that second window in step (2) above and you can then navigate that second window to a page within your application.

To experiment with this I put together this little test UI;

image

which is using a Frame navigated to a page /Pages/MainWindow.xaml and when I click the Open Window button it uses HtmlPage.PopupWindow to open up the same HTML page with the same XAP but asking it to navigate to #Pages/SecondWindow.xaml so I end up with 2 windows;

image

the windows are then using LocalMessageSender/LocalMessageReceiver to talk to each other and I can add a 3rd one by clicking the button again;

image

and then I can communicate with the 2 other windows either by sending specifically to one of them;

image

or I can broadcast some data to both of them;

image

and they can reply back to their parent window if they have something to report back although I didn’t write the code to allow the child windows to broadcast;

image

and as the child windows close, the parent should know about it;

image

and it disables the UI when the last one goes away;

image

so, that’s all “well and good” and I wrote a quick class ( which could be improved a heck of a lot in a number of ways ) in order to try and have a limited little “communication channel” between these windows. As below;

  public class WindowMessageEventArgs : EventArgs
  {
    public string MessageBody { get; set; }
  }
  public class WindowOpenedClosedEventArgs : EventArgs
  {
    public string WindowName { get; set; }
  }
  public class WindowManager : INotifyPropertyChanged
  {
    public event EventHandler<WindowMessageEventArgs> MessageReceived;
    public event EventHandler<WindowOpenedClosedEventArgs> WindowOpened;
    public event EventHandler<WindowOpenedClosedEventArgs> WindowClosed;
    public event PropertyChangedEventHandler PropertyChanged;

    public WindowManager() : this(mainWindowName)
    {
    }
    public WindowManager(string windowName)
    {
      this.ChildWindows = new ObservableCollection<string>();

      this.ChildWindows.CollectionChanged += (s, e) =>
      {
        RaisePropertyChanged("HasChildWindows");
        RaisePropertyChanged("ChildWindowCount");
      };

      this.windowName = windowName;

      if (this.windowName != mainWindowName)
      {
        this.windowName += "_" + Guid.NewGuid().ToString();

        // TBD: perhaps not a good idea?
        Application.Current.Exit += OnExit;
      }

      this.messageReceiver = new LocalMessageReceiver(
        this.windowName);

      this.messageReceiver.MessageReceived += OnMessageReceived;

      this.messageReceiver.Listen();
    }
    public ObservableCollection<string> ChildWindows 
    { 
      get; set; 
    }
    public int ChildWindowCount
    {
      get
      {
        return (this.ChildWindows.Count);
      }
    }
    public bool HasChildWindows
    {
      get
      {
        return (this.ChildWindowCount != 0);
      }
    }  
    public void NavigateToPageInNewWindow(string page)
    {
      string schemeAndServer =
        HtmlPage.Document.DocumentUri.GetComponents(
        UriComponents.SchemeAndServer, UriFormat.Unescaped);

      string path = HtmlPage.Document.DocumentUri.AbsolutePath;

      Uri newUri = new Uri(schemeAndServer + path + string.Format("#{0}", page), 
        UriKind.Absolute);
      
      HtmlWindow window = 
        HtmlPage.PopupWindow(
          newUri,
         "_blank",
         new HtmlPopupWindowOptions()
         {
           Directories = false,
           Menubar = false,
           Status = false,
           Toolbar = false,
           Resizeable = true
         });

      int x = 10;
    }
    public void Broadcast(string message)
    {
      if (this.windowName != mainWindowName)
      {
        throw new InvalidOperationException("Only broadcast from main window");
      }
      foreach (string window in this.ChildWindows)
      {
        SendToChild(window, message);
      }
    }
    public void AnnounceOpening()
    {
      if (this.windowName == mainWindowName)
      {
        throw new InvalidOperationException();
      }
      SendToParent(string.Format("{0}{1}",
        helloMessage, this.windowName));
    }
    public void AnnounceClosing()
    {
      if (this.windowName == mainWindowName)
      {
        throw new InvalidOperationException();
      }
      SendToParent(string.Format("{0}{1}",
        goodbyeMessage, this.windowName));
    }
    public void SendToParent(string message)
    {
      SendMessage(mainWindowName, message);
    }
    public void SendToChild(string childWindow, string message)
    {
      SendMessage(childWindow, message);
    }
    void RaisePropertyChanged(string property)
    {
      if (this.PropertyChanged != null)
      {
        this.PropertyChanged(this, new PropertyChangedEventArgs(property));
      }
    }
    void OnExit(object sender, EventArgs e)
    {
      AnnounceClosing();
    }
    void OnMessageReceived(object sender, MessageReceivedEventArgs e)
    {
      if (this.windowName != mainWindowName)
      {
        FireMessageReceived(e.Message);
      }
      else
      {
        if (e.Message.StartsWith(helloMessage))
        {
          string message = e.Message.Substring(
            helloMessage.Length);

          this.ChildWindows.Add(message);

          if (WindowOpened != null)
          {
            WindowOpened(this, new WindowOpenedClosedEventArgs() { WindowName = message });
          }
        }
        else if (e.Message.StartsWith(goodbyeMessage))
        {
          string message = e.Message.Substring(
            goodbyeMessage.Length);

          this.ChildWindows.Remove(message);

          if (WindowClosed != null)
          {
            WindowClosed(this, new WindowOpenedClosedEventArgs() { WindowName = message });
          }
        }
        else
        {
          FireMessageReceived(e.Message);
        }
      }
    }
    void FireMessageReceived(string message)
    {
      if (this.MessageReceived != null)
      {
        this.MessageReceived(
          this,
          new WindowMessageEventArgs()
          {
            MessageBody = message
          });
      }
    }   
    void SendMessage(string windowName, string message)
    {
      LocalMessageSender sender = new LocalMessageSender(windowName);
      sender.SendAsync(message);
    }
    LocalMessageReceiver messageReceiver;
    string windowName;

    const string helloMessage = "hello:";
    const string goodbyeMessage = "bye:";
    const string mainWindowName = "MainWindow";
  }

and so the essence of this is that the “Main Window” is using a well known “MainWindow” name to communicate via LocalMessageSender/LocalMessageReceiver and each child window gets its own unique name and is meant to “announce” itself to its main window when it arrives on the scene and also say “goodbye” when it is shut down.

The interaction then runs something like this;

  1. Application runs up.
  2. Frame navigates to /Pages/MainWindow.xaml
  3. MainWindow instantiates a WindowManager and binds to its ChildWindowCount, HasChildWindows properties.
  4. When the user asks to open the child window it uses WindowManager.NavigateToPageInNewWindow( “/Pages/SecondWindow.xaml” )
  5. Application runs up again in a second window.
  6. 2nd copy of the application navigates to /Pages/SecondWindow.xaml.
  7. 2nd copy of the application announces itself back to the MainWindow via LocalMessageSender and opens up its own local LocalMessageReceiver using a unique name to refer to its window instance.
  8. First copy of the application now has the window name of the newly created window.
  9. Windows (apps) can now “chat” acting potentially as a single application.
  10. Second copy of the application says goodbye by sending a final message to the MainWindow.

There’s a lot of assumptions and “code behind” in there and it could be improved a lot but it basically works.

Here’s the project.

Out Of The Browser

What to do about out of browser? Above, I’m relying on the HtmlPage.PopupWindow trick to be able to run my application more than once.

As far as I know, there’s no equivalent if you’re out of browser, there’s no way in an out-of-browser application of saying “run this current application for me again and pass it a bit of state to tell it to navigate to a particular page when it runs”.

The way in which out of browser applications are run is via the sllauncher process and if I have some app called foo.xap which I installed from http://localhost then I can see that the sllauncher process is being run with;

"C:\Program Files\Microsoft Silverlight\sllauncher.exe" 3665049860.localhost

So perhaps I could use that but to run a secondary program like this is going to require my out of browser application to be trusted and it’ll only work on Windows as I’ll have to run the program via COM interop to the shell.

Also, that gives me no way to pass some state to the application to tell it that it is playing the part of a secondary window. Awkwardly, it would rely on me getting to that 366* number which isn’t something that I can get hold of.

That name corresponds to a folder on disk which I can easily find in;

c:\users\username\appdata\local\microsoft\silverlight\outofbrowser

but I’m not sure that it would always be there because I think it might go into appdata\locallow instead ( not sure ) and it’s certainly not documented and so liable to change without notice and so anything built on this knowledge is going to be a hack.

Another way to use sllauncher.exe is with the /emulate option which ( given a XAP ) can run it up for me – Visual Studio uses this when you do the “debug as out of browser” trick without first installing the application locally.

But, again, how to know the location of the XAP? It lives in that magically named folder.

What if I gave up on trying to guess the location of the XAP and copied it somewhere else so that I knew exactly where it was?

Note – this is still shaky because I’m only considering the single XAP case and I’d have to make sure that whenever the application updated itself I copied it again to ensure the copy didn’t get out of sync.

So, at some point in the lifetime of my (trusted, out-of-browser) application I might run code like;

      WebClient client = new WebClient();

      client.OpenReadCompleted += (s, e) =>
        {
          if (e.Error == null)
          {
            try
            {
              string xapFileName = Application.Current.Host.Source.LocalPath;

              int index = xapFileName.LastIndexOf('/');

              if (index != -1)
              {
                xapFileName = xapFileName.Substring(index + 1);
              }

              using (FileStream fs = File.OpenWrite(
                System.IO.Path.Combine(Environment.GetFolderPath(
                  System.Environment.SpecialFolder.MyDocuments), xapFileName)))
              {
                e.Result.CopyTo(fs);
                fs.Close();
              }
            }
            finally
            {
              e.Result.Close();
            }       
          }
        };

      client.OpenReadAsync(Application.Current.Host.Source);

and so I “know” that my XAP file is now in MyDocuments ( or, at least, I hope it makes it there Smile ).

Knowing it’s in MyDocuments means I might be able to launch it with a fragment something like;

    if (AutomationFactory.IsAvailable)
      {
        string command =
          string.Format(
          "\"%ProgramFiles%\\Microsoft Silverlight\\sllauncher.exe\" " +
          "/emulate:\"{0}\" " +
          "/origin:\"{1}",
          xapFileName,
          originToUse);

        dynamic exec = AutomationFactory.CreateObject("WScript.Shell");        
        
        exec.Run(command, 1, true);
      }

and that does kind of work but then I hit a bit of a stumbling block ( you ever get that feeling that you should just give up? ) in that it was only at this point that I realised that the /emulate option effectively runs up the application once.

That is, I had a little test harness “working”;

image

but then hitting the “Run New Instance” for a second time did not raise another instance of the app, it just gave up.

I realised that if I appended some unique fragment onto the end of the origin URL that I was passing to sllauncher.exe then I could get multiple instances to launch in the sense of;

image

so “maybe” there was a possibility of getting this to “sort of work” as the fragment is also a way of getting across some initial state into the application which I need to tell it which page to navigate to.

I took this approach back to my original test application and hacked around with it a little and it works reasonably well. I added a new button to the UI;

image

so, the application forces you to click this button before it allows use of the other buttons.

This button will then attempt to copy the XAP down to;

c:\users\mydocuments\whateverTheFileNameIs.xap

once that’s done, it enables the rest of the “UI” and the “Open Window” button becomes available again and clicking on that now goes ahead and runs;

sllauncher /emulate:c:\users\mydocuments\whateverTheFileNameIs.xap /origin:URL

now the URL here is a lot of a hack. This application only has one 2nd window which is in /Pages/SecondWindow.xaml and so I construct a URL something like;

URL=http://localhost:32768#/Pages/SecondWindow.xaml:GUID

the only reason the GUID is there is to make the URL unique so that if I want to have 2 copies of SecondWindow.xaml on the screen then the /emluate option won’t stop me.

This then launches a second local copy of the application;

image

and I could launch 3rd and 4th copies and have them “talk” to each other like they did before.

image

Summing Up

That was mostly just a bit of fun to see how far I could take this and where it would get me Smile 

If I was putting together a real application that needed multiple windows then I could see it might be possible to make it work based on this kind of approach although, naturally, it’s not ideal to have to run the application each time to open a window and nor is it ideal that there’s no shared state between the windows but, instead, they are just passing messages ( which I coded as simple strings here but could grow up to be serialized objects ) and there are other hacky things in the implementation here.

Here’s the second version of the project in case you want to take a look – bear in mind the manner in which it was put together (i.e. quickly and for experimentation).