読者です 読者をやめる 読者になる 読者になる

Silverlight 2でTracのUIを作ってみる

(2008/10/07追記) 本エントリーの内容は、当初Silverlight 2 RC0で開発したものを記述していましたが、RC0で開発したプログラムは一般公開できないとのことなので、Beta2で開発したものに修正しました。

Tracのチケット周りのUIって、ちょっと使いにくいよね。

なので以下の記事を参考にSilverlightでUIを作ろうと思い試してみたんだけど、最新のTracLightning-2.0.9では上手く動かない。


どうやら、Trac-0.11からWikiマクロの作り方が変わったみたいだ。

というわけで、coma2nさんのマクロをTrac-0.11に対応させてみました。(と言っても大した変更ではないですが。)

なおSilverlight.pyの置き場所は「wiki-macros」ディレクトリではなく「plugins」ディレクトリです。

Silverlight.py

# -*- coding: UTF-8 -*-
"""
Silverlightコンテンツを埋め込みます。

Silverlightコンテンツはプロジェクト毎の「htdocs」ディレクトリに「ClientBin」というディレクトリ
を作り、その下に配置する必要があります。

ex.
[[Silverlight(src=Hoge.xap)]]

[[Silverlight(src=Hoge.xap, width=200, height=200)]]
"""
from trac.wiki.macros import WikiMacroBase
from StringIO import StringIO

class SilverlightMacro(WikiMacroBase):
  def expand_macro(self, formatter, name, args):
    params = {}
    for arg in args.split(','):
      val = arg.split('=')
      params[val[0].strip()] = val[1]

    xap_width, xap_height = 100, 100
    xap_src = self.env.abs_href.chrome('site', 'ClientBin', params['src'])

    if params.has_key('width'): xap_width = params['width']
    if params.has_key('height'): xap_height = params['height']

    buf = StringIO()
    buf.write(
    '<object data="data:application/x-silverlight," type="application/x-silverlight-2-b2" width="%(xap_width)s" height="%(xap_height)s">' \
    '<param name="source" value="%(xap_src)s" />' \
    '<param name="background" value="white" />' \
    '<a href="http://go.microsoft.com/fwlink/?LinkID=115261" style="text-decoration: none;">' \
    '<img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style: none"/>' \
    '</a>' \
    '</object>' % vars()
    )
    return buf.getvalue()


続いてこのマクロを利用して、クエリを指定してチケットを取得するSilverlightアプリを作ってみました。

かなり適当な実装なので公開するのもちょっと恥ずかしいのですが・・・。以下、気になっている点。

  • 認証が通らないので、XML-RPCプラグインはanonymousにアクセス権を与えている。
  • XMLのパースが無理矢理。もうちょっときれいに書きたい。
  • ComboBoxのSelectedItemに値をセットしても、選択された状態にならない?(なのでSelectedIndexで指定)
  • DataGridのItemSourceは、いったんnullをセットしないと更新できない?
  • ComboBoxItemは、StaticResourceで指定できないだろうか。

Page.xaml

<UserControl xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"  x:Class="SilverTrac.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:SilverTrac"
    Width="800" Height="600">
    
    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Query"></TextBlock>
            <TextBox x:Name="TextBoxQuery" Width="200"></TextBox>
            <Button Content="Update" Click="Button_Click"/>
        </StackPanel>
        <data:DataGrid x:Name="GrdTicket" AutoGenerateColumns="True"/>
        <TextBlock x:Name="dispText"></TextBlock>
    </StackPanel>
</UserControl>

Page.xaml.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Xml.Linq;

namespace SilverTrac
{

    public partial class Page : UserControl
    {
        private Dictionary<string, Ticket> ticketList = new Dictionary<string, Ticket>();

        public Page()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            GetXmlRpc("http://localhost/trac/Sample/xmlrpc", "ticket.query", TextBoxQuery.Text, TicketQuery);
        }

        private void TicketQuery(string response)
        {
            XDocument xd = XDocument.Parse(response);
            List<XElement> values = xd.Root.Element("params").Element("param").Element("value").Element("array").Element("data").Elements("value").ToList();

            foreach (var val in values)
            {
                GetXmlRpc("http://localhost/trac/Sample/xmlrpc", "ticket.get", int.Parse(val.Value), GetTicket);
            }
        }

        private void GetTicket(string response)
        {

            XDocument xd = XDocument.Parse(response);

            Ticket t = new Ticket();
            List<XElement> values = xd.Root.Element("params").Element("param").Element("value").Element("array").Element("data").Elements("value").ToList();
            t.Id = values[0].Value;
            t.Created = values[1].Value;
            t.LastModified = values[2].Value;

            List<XElement> members = values[3].Element("struct").Elements("member").ToList();

            t.Status = members[0].Element("value").Value;
            t.Description = members[1].Element("value").Value;
            t.Reporter = members[2].Element("value").Value;
            t.Cc = members[3].Element("value").Value;
            t.Resolution = members[4].Element("value").Value;
            t.Component = members[5].Element("value").Value;
            t.DueAssign = members[6].Element("value").Value;
            t.Summary = members[7].Element("value").Value;
            t.Priority = members[8].Element("value").Value;
            t.Keywords = members[9].Element("value").Value;
            t.Version = members[10].Element("value").Value;
            t.Milestone = members[11].Element("value").Value;
            t.Owner = members[12].Element("value").Value;
            t.Type = members[13].Element("value").Value;
            t.DueClose = members[14].Element("value").Value;

            if (!ticketList.ContainsKey(t.Id))
            {
                ticketList.Add(t.Id, t);
            }

            GrdTicket.ItemsSource = null;
            GrdTicket.ItemsSource = ticketList.Values;

        }

        public void GetXmlRpc(string reqURL, string methodName, object arguments, Action<string> callback)
        {
            string reqXml =
@"<?xml version=""1.0"" encoding=""UTF-8""?>
<methodCall>
  <methodName>" + methodName + @"</methodName>
  <params>";
            reqXml += GetArgumentXML(arguments);
            reqXml += @"
  </params>
</methodCall>";

            byte[] content = System.Text.Encoding.UTF8.GetBytes(reqXml);

            WebRequest webReq = WebRequest.Create(new Uri(reqURL));

            webReq.Method = "POST";
            webReq.ContentType = "text/xml";

            webReq.BeginGetRequestStream(r1 =>
            {
                try
                {
                    using (var stm = webReq.EndGetRequestStream(r1))
                    {
                        stm.Write(content, 0, content.Length);
                    }
                    webReq.BeginGetResponse(r2 =>
                    {
                        try
                        {
                            var webRes = webReq.EndGetResponse(r2);
                            using (var sr = new StreamReader(webRes.GetResponseStream()))
                            {
                                string response = sr.ReadToEnd();

                                Dispatcher.BeginInvoke(() =>
                                {
                                    callback(response);
                                });
                            }
                        }
                        catch (Exception ex)
                        {
                            Dispatcher.BeginInvoke(() => { dispText.Text = ex.InnerException.Message; });
                            return;
                        }
                    }, null);
                }
                catch (Exception ex)
                {
                    Dispatcher.BeginInvoke(() => { dispText.Text = ex.Message; });
                    return;
                }
            }, null);

        }

        Dictionary<Type, string> typeDict = new Dictionary<Type, string>()
        {
            {typeof(int), "int"},
            {typeof(string), "string"}
        };

        private string GetArgumentXML(object arg)
        {
            string xml = string.Empty;

            if (arg == null) return xml;

            xml += @"
    <param>
      <value>
        <" + typeDict[arg.GetType()] + ">" + arg.ToString() + "</" + typeDict[arg.GetType()] + ">" + @"
      </value>
    </param>";

            return xml;
        }

    }

    public class Ticket
    {
        public string Id { get; set; }
        public string Created { get; set; }
        public string LastModified { get; set; }
        public string Status { get; set; }
        public string Description { get; set; }
        public string Reporter { get; set; }
        public string Cc { get; set; }
        public string Resolution { get; set; }
        public string Component { get; set; }
        public string DueAssign { get; set; }
        public string Summary { get; set; }
        public string Priority { get; set; }
        public string Keywords { get; set; }
        public string Version { get; set; }
        public string Milestone { get; set; }
        public string Owner { get; set; }
        public string Type { get; set; }
        public string DueClose { get; set; }
        public string Attributes { get; set; }
    }
}

今後は、もう少し使えるアプリを開発しようと思っています。