静的なSilverlightコンテンツを動的に生成する

この前eラーニング用のコンテンツを作るアプリを作ったけど(途中で面倒くさくなって未完成だけどね)、このアプリでは作ったコンテンツの情報はXMLに保存されていて、その情報を元にUIを生成していた。

つまりSilverlightコンテンツ(XAP)は一つで、パラメータによって表示するコンテンツを変えていることになる。

実際にSilverlightコンテンツをホストするHTMLを見てみると以下のようになっている。

<object type="application/x-silverlight" data="data:application/x-silverlight," id="ctl00_MainContent_Xaml1" style="height:100%;width:100%;">
  <param name="InitParams" value="id=2"></param>
  <param name="source" value="ClientBin/eLearn.xap"/>
  <a href="http://go2.microsoft.com/fwlink/?LinkID=114576&amp;v=1.0">
      <img src="http://go2.microsoft.com/fwlink/?LinkID=108181" alt="Get Microsoft Silverlight" style="border-width:0;" />
  </a>
</object>

paramタグでInitParamsというプロパティに「id=2」という値を渡している。Silverlight側ではこの値を取り出して、コンテンツを生成しているわけだ。

別にこれでも問題は無いんだけど、この方法でいくとInitParamsに渡す引数さえ変えてやれば他のコンテンツにも自由にアクセスすることができてしまう。これはメリットでもありデメリットでもあるんだけど、これはセキュリティ的によろしくないんじゃないだろうか?まぁその先でアクセス制御をしろという話もあるだろうけど。

どっちにしろ動的にコンテンツを生成するよりも、いっそのこと静的なコンテンツを動的に生成してしまったらいいと考えた。そうすればシステムから切り離せるので、手離れがよくなって、ユーザは生成されたXAPをどこにでも埋め込む事ができるようになる。

Silverlightアセンブリコンパイル

動的にXAPファイルを生成するということはC#XAMLソースコードを動的にコンパイルしてまとめるということなので、まずはSilverlightをターゲットにしたアセンブリコンパイル方法から調べてみた。

コンパイルにはCodeDomを使用するんだけど、Silverlight側にはたいした物が用意されていないのでサーバーサイド(ASP.NET)でやる。

適当にWebプロジェクトを作って、「Compile.aspx」というWebページを追加する。
そして「StaticContents」というSilverlightプロジェクトを作って、そのソースファイルをWebプロジェクトの「App_Data/src」というフォルダにコピーしておく。

で、そのソースファイルをコンパイルするコードが以下。

Compile.aspx.cs
using System;
using System.IO;
using System.Web;
using System.Linq;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;

using Microsoft.CSharp;

namespace SilverlightApplication2_Web {
   public partial class Compile : System.Web.UI.Page {
       protected void Page_Load(object sender, EventArgs e) {
           var srcPath = Server.MapPath("~/App_Data/src");
           var sourceFiles = Directory.GetFiles(srcPath, "*.cs");

           var installRoot = @"C:\Program Files\Microsoft Silverlight\2.0.30523.6";
           var assemblyFiles = new[] {
               "mscorlib.dll",
               "System.dll",
               "System.Xml.dll",
               "System.Core.dll",
               "System.Windows.dll"

           }.Select(f => Path.Combine(installRoot, f)).ToArray();

           var providerOptions = new Dictionary<string, string>() {
               { "CompilerVersion", "v3.5" }
           };
           using(var provider = new CSharpCodeProvider(providerOptions)) {
               var options = new CompilerParameters(assemblyFiles) {
                   GenerateExecutable = false,
                   OutputAssembly = "StaticContents.dll",
                   GenerateInMemory = false,
                   CompilerOptions = "/nostdlib"
               };
               var result = provider.CompileAssemblyFromFile(options, sourceFiles);

               Response.ContentType = "text/plain";
               Response.Write(string.Join("\n", result.Output.Cast<string>().ToArray()));
               Response.End();
           }
       }
   }
}

CompilerParametersクラスのCompilerOptionsに「/nostdlib」を設定するのがミソ。これをやらないと.NETのmscorlib.dllを読み込んでしまう。

結果から言えばこのコードは動かない。XAMLから生成されるpartialクラスと連結しないといけないから。でもXAMLからC#コードを生成する方法がわからなかったのと、このやり方はだいぶ面倒くさい事がわかったのでやめた。

XAPファイルの生成

よく考えたら、Silverlightプロジェクトのプロジェクトファイルをそのまま使ってMSBuildでビルドすればいいだけだと気付いた。

MSBuildを使って生成するコードが以下。

Compile.aspx.cs

using System;
using System.IO;
using System.Web;
using System.Linq;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;

using Microsoft.CSharp;

using Microsoft.Build.Framework;
using Microsoft.Build.BuildEngine;

namespace SilverlightApplication2_Web {
   public partial class Compile : System.Web.UI.Page {
       protected void Page_Load(object sender, EventArgs e) {
           var engine = new Engine();
           engine.RegisterLogger(new HttpResponseLogger(Response));
            
           var project = new Project(engine);
           project.Load(Server.MapPath("~/App_Data/src/StaticContents.csproj"));
           project.Build();

           engine.Shutdown();
       }

       class HttpResponseLogger : ILogger {
           private HttpResponse response;

           public HttpResponseLogger(HttpResponse response) {
               this.response = response;
               this.response.Output.Write("<html><body style='font-size:x-small;'>");
           }

           public void Initialize(IEventSource eventSource) {
               eventSource.ProjectStarted += (s, e) => response.Output.Write("<span style='color:blue;'>{0}</span><br>", e.Message);
               eventSource.MessageRaised += (s, e) => response.Output.Write("<span>{0}</span><br>", e.Message);
               eventSource.ErrorRaised += (s, e) => response.Output.WriteLine("<span style='color:red;'>{0}</span><br>", e.Message);
               eventSource.ProjectFinished += (s, e) => response.Output.WriteLine("<span style='color:blue;'>{0}</span><br>", e.Message);
               eventSource.TargetStarted += (s, e) => response.Output.Write("<span style='color:green;'>{0}</span><br>", e.Message);
               eventSource.TargetFinished += (s, e) => response.Output.Write("<span style='color:green;'>{0}</span><br>", e.Message);
           }
           public string Parameters {
               get;
               set;
           }
           public void Shutdown() {
               this.response.Output.Write("</body></html>");
               this.response.End();
           }
           public LoggerVerbosity Verbosity {
               get;
               set;
           }
       }
   }
}

HttpResponseLoggerクラスはMSBuildの出力をHTMLで出力するためのロガー。

これでXAPファイルを生成できるので、これを利用すればSilverlight側でXAMLを定義してそれをサーバーサイドに送信して、ビルドするプロジェクトにくっつけてビルドしたXAPファイルをレスポンスとして返してやったりすることができるので、おもしろいことができそうな予感。

でもこれってよく考えたらDyanmicSilverilghtがやってることやん!!orz

[追記]
これを正常に実行するためにアセンブリのバージョンをリダイレクトする設定が必要なのを忘れていた。

以下がWeb.configに必要な設定

Web.config
<runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
        <dependentAssembly>
            <assemblyIdentity name="Microsoft.Build.Framework"
                              publicKeyToken="b03f5f7f11d50a3a"
                              culture="neutral" />
            
            <bindingRedirect oldVersion="2.0.0.0" newVersion="3.5.0.0" />
        </dependentAssembly>
    </assemblyBinding>
</runtime>