Silverlightで作る付箋紙アプリ その4

今回は付箋紙の追加、編集、削除のサーバーサイドの処理を実装していく。

付箋紙を追加する

付箋紙を追加するためのURLは「~/Item/New.aspx」にする。付箋紙を追加するためのパラメータは左端の位置(left)と上端の位置(top)だけ。これをクエリパラメータとして渡す。

ASP.NET MVCフレームワークではクエリパラメータとして渡した値は自動的にメソッドの引数としてマップされるので、URLルーティングのルールは前回のままで変更しなくていい。

Controllers/ItemController.cs

using System;
using System.IO;
using System.Web.Mvc;
using System.Xml.Linq;
using System.Text;
using System.Diagnostics;

namespace Fusen.Web.Controllers {
   /// <summary>
   /// 付箋紙アイテムに対するアクセスを提供するコントローラクラス
   /// </summary>
   public class ItemController : Controller {
       /// <summary>
       /// 付箋紙の情報を保存するファイル
       /// </summary>
       private const string DATA_FILE_PATH = "~/App_Data/items.xml";

       public ItemController() {
       }

       /// <summary>
       /// 付箋紙情報の一覧を取得します。
       /// </summary>
       public void List() {
           var fileName = Request.MapPath(DATA_FILE_PATH);

           using(var sr = new StreamReader(fileName, Encoding.UTF8)) {
               Response.ContentEncoding = Encoding.UTF8;
               Response.ContentType = "text/xml";
               Response.Output.Write(sr.ReadToEnd());
               Response.End();
           }
       }

       /// <summary>
       /// 指定した情報で付箋紙の情報を追加します。
       /// </summary>
       /// <param name="left">左端の位置</param>
       /// <param name="top">上端の位置</param>
       /// <param name="width">横幅</param>
       /// <param name="height">縦高さ</param>
       /// <param name="color">色の名前</param>
       public void New(double? left, double? top, double? width, double? height, string color) {
           var fileName = Request.MapPath(DATA_FILE_PATH);

           var id = DateTime.Now.ToString("yyyyMMdd-HHmmssffff");
           var xml = XDocument.Load(fileName);
           xml.Root.Add(new XElement("item", new[] {
               new XElement("id", id),
               new XElement("left", left), new XElement("top", top),
               new XElement("width", width), new XElement("height", height),
               new XElement("color", color),
               new XElement("comment")
           }));
           xml.Save(fileName);

           Response.ContentEncoding = Encoding.UTF8;
           Response.ContentType = "text/plain";
           Response.Output.Write(id);
           Response.End();
       }
   }
}

解説

付箋紙のXMLファイルのパスを「DATA_FILE_PATH」という定数にした。

public void New(double? left, double? top, double? width, double? height, string color) {
}

double?型で「left」、「top」、「width」、「height」、文字列型で「color」という引数を持つNewメソッドを定義している。
double型をNullableにしているのは、パラメータを指定されなかった時にわかりやすくするため(指定されないとnullになる)。といっても現状はその処理を入れていない。

処理的には付箋紙のXMLファイルを開いてルート要素に新しくitem要素を追加している。id属性は現在の日付から一意になるような文字列を生成して割り当てている。あとはそれぞれの引数に対応する要素を追加している。コメントは空の要素にしておく。

Response.ContentEncoding = Encoding.UTF8;
Response.ContentType = "text/plain";
Response.Output.Write(id);
Response.End();

追加に成功するとレスポンスとしてidを出力するようにしている。

これで「http://localhost:1100/Item/New.aspx?left=10&top=20」というようなURLにアクセスすると新しい付箋紙が追加されて、生成されたidが画面に表示されるはず。

次はこのURLにアクセスして付箋紙を追加するSilverlight側の処理を実装する。

Page.xaml.cs

using System;
using System.Net;
using System.Xml.Linq;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Browser;
using System.Windows.Controls;

using Fusen.Controls;

namespace Fusen {
   public partial class Page : UserControl {
       private string rootUrl = "http://localhost:1100/";
       /// <summary>
       /// 前回クリックされた時間
       /// </summary>
       private DateTime lastClickTime = DateTime.MinValue;

       public Page() {
           InitializeComponent();
       }

       /// <summary>
       /// 付箋紙の一覧を読み込みます。
       /// </summary>
       private void LoadItems() {
           var webClient = new WebClient();
           webClient.DownloadStringCompleted += (s, e) => {
               var xml = XDocument.Parse(e.Result);
               var result = from item in xml.Descendants("item")
                            select new Item {
                                Id = (string)item.Element("id"),
                                Left = (double)item.Element("left"),
                                Top = (double)item.Element("top"),
                                Width = (double)item.Element("width"),
                                Height = (double)item.Element("height"),
                                Comment = (string)item.Element("comment"),
                                Color = (string)item.Element("color")
                            };

               foreach(var item in result) {
                   var itemCtrl = new ItemControl {
                       DataContext = item
                   };
                   LayoutRoot.Children.Add(itemCtrl);
               }
           };
           webClient.DownloadStringAsync(new Uri(rootUrl + "Item/List.aspx"));
       }

       /// <summary>
       /// 指定した位置に新しく付箋紙を追加します。
       /// </summary>
       /// <param name="location">追加する位置</param>
       private void NewItem(Point location) {
           var item = new Item {
               Left = location.X, Top = location.Y,
               Width = 200, Height = 150,
               Color = "Yellow"
           };
           var webClient = new WebClient();
           webClient.DownloadStringCompleted += (s, e) => {
               if(e.Error != null) {
                   HtmlPage.Window.Alert("付箋紙の追加に失敗しました。");

               } else {
                   item.Id = e.Result;

                   LayoutRoot.Children.Add(new ItemControl {
                       DataContext = item
                   });
               }
           };
           webClient.DownloadStringAsync(new Uri(rootUrl +
               string.Format("Item/New.aspx?left={0}&top={1}&width={2}&height={3}&color={4}",
                   item.Left, item.Top, item.Width, item.Height, item.Color
               )
           ));
       }

       private void UserControl_Loaded(object sender, RoutedEventArgs e) { LoadItems(); }

       private void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
           var now = DateTime.Now;
           // ダブルクリックされた!!
           if((now - lastClickTime).TotalMilliseconds <= 200) {
               NewItem(e.GetPosition(this));
           }
           lastClickTime = now;
       }
   }
}

解説

以前のメイン画面でダブルクリックされた時の処理は付箋紙コントロールインスタンス化して、LayoutRootの子コントロールとして追加していたけど、今回はNewItemというメソッドを定義してそれを呼び出すようにしている。

NewItemメソッドではItemクラスに初期パラメータを与えてインスタンス化し、その情報で先程の付箋紙追加用のURLにリクエストを送っている。

var item = new Item {
    Left = location.X, Top = location.Y,
    Width = 200, Height = 150,
    Color = "Yellow"
};

Itemクラスのインスタンス化時にUIの情報が入っているのが気持ち悪いけど、今はいい方法が思いつかない*1

あとはレスポンスが返ってきてから付箋紙コントロールインスタンス化して、そのDataContextプロパティにItemオブジェクトを渡してLayoutRootの子コントロールとして追加している。

これでビルドして実行すれば、メイン画面情報をダブルクリックして追加された付箋紙が次回起動時も表示されるのが確認できる。

付箋紙を削除する

付箋紙を削除するためのURLは「~/Item/Delete.aspx」にする。削除したい付箋紙のidをクエリパラメータとして渡す。

Deleteメソッドの実装は以下。

Controllers/ItemController.cs
/// <summary>
/// 指定したidの付箋紙を削除します。
/// </summary>
/// <param name="id">付箋紙のid</param>
public void Delete(string id) {
  var fileName = Request.MapPath(DATA_FILE_PATH);

  var xml = XDocument.Load(fileName);
  var result = from item in xml.Descendants("item")
               where (string)item.Element("id") == id
               select item;

  foreach(var item in result.ToList()) item.Remove();

  xml.Save(fileName);
}

付箋紙のXMLデータを開いて、削除するidの付箋紙を検索する。検索して引っ掛かったノードを順次削除していく。
このforeachの時のコレクションにresult変数をそのまま渡してしまうと、列挙の最中にXMLデータの内容が変わってしまうため例外が出るので、一度ToListメソッドでListに変換してから列挙してやる必要がある。

次はSilverilght側だけど、まだ付箋紙を削除するためのボタンが無いので、まずはそれから追加する。

Controls/ItemControl.xaml

<UserControl x:Class="Fusen.Controls.ItemControl"
   xmlns="http://schemas.microsoft.com/client/2007"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Width="{Binding Width, Mode=TwoWay}" Height="{Binding Height, Mode=TwoWay}"
   Canvas.Left="{Binding Left, Mode=TwoWay}" Canvas.Top="{Binding Top, Mode=TwoWay}"
   Loaded="UserControl_Loaded" SizeChanged="UserControl_SizeChanged">
    
   <UserControl.Resources>
       <Style x:Key="commandButton" TargetType="Button">
           <Setter Property="Template">
               <Setter.Value>
                   <ControlTemplate TargetType="Button">
                       <ContentPresenter Content="{TemplateBinding Content}"
                                         FontSize="{TemplateBinding FontSize}"
                                         Cursor="Hand"
                                         HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                                         VerticalAlignment="{TemplateBinding VerticalAlignment}"
                                         />
                   </ControlTemplate>
               </Setter.Value>
           </Setter>
       </Style>
   </UserControl.Resources>
    
   <Grid>
       <Polygon x:Name="resizeArea" Fill="{Binding Brush}" Opacity="0.6" Stroke="Black" Points="0,0 0,150, 175,150, 200, 125, 200,0" />
       <Polygon x:Name="resizeGrip" Fill="White" Stroke="Silver" Points="175,150, 175,125, 200,125" Cursor="Hand"
                MouseLeftButtonDown="resizeGrip_MouseLeftButtonDown" MouseLeftButtonUp="resizeGrip_MouseLeftButtonUp" />

       <Border x:Name="dragGrip" BorderBrush="Black" Width="20" Height="20" CornerRadius="10" Cursor="Hand"
               HorizontalAlignment="Left" VerticalAlignment="Top" Margin="6,6,0,0"
               MouseLeftButtonDown="dragGrip_MouseLeftButtonDown" MouseLeftButtonUp="dragGrip_MouseLeftButtonUp">
           <Border.Background>
               <RadialGradientBrush>
                   <GradientStop Color="White" Offset="0" />
                   <GradientStop Color="Silver" Offset="1" />
               </RadialGradientBrush>
           </Border.Background>
       </Border>
        
       <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
           <Button x:Name="editButton" Content="edit" Margin="0,4,6,0"
                   Click="editButton_Click" Style="{StaticResource commandButton}" />
           <Button x:Name="deleteButton" Content="x" Margin="0,0,4,0"
                   Click="deleteButton_Click"  Style="{StaticResource commandButton}" />
       </StackPanel>
        
       <TextBlock x:Name="commentText" Text="{Binding Comment}" TextWrapping="Wrap"
                  FontSize="12" FontFamily="MS UI Gothic" Margin="5,35,5,30" />
       <TextBox x:Name="commentInput" Text="{Binding Comment, Mode=TwoWay}" AcceptsReturn="True" Visibility="Collapsed"
                FontSize="12" FontFamily="MS UI Gothic" Margin="5,35,5,30" />
   </Grid>

</UserControl>

スクリーンショット

付箋紙の右上端に「x」ボタンと「edit」ボタンを追加している。二つとも共通の外観(テキストだけのボタン)なので、Styleに切り離してUserControlのResourceとして定義している。
あと、それぞれのボタンのClickイベントに対応したイベントハンドラも追加しておいた。

次に「x」ボタンが押された時のイベントハンドラを実装するわけだけど、ここで実際に付箋紙を削除する処理をやるわけにはいかない。何故かと言えば、付箋紙コントロールはあくまでUI部品であって、なんらかのビジネスロジックを内包するのは役割が違う。それにこいつがWebリクエストを送るなんて気持ちが悪過ぎる。

じゃあ、どうするかというと「x」ボタンが押された事をイベントとして発行し、外部に知らせてやればいい。あとはイベントを受け取った側が煮るなり焼くなり好きにすればいい。

イベントを定義するためにまず、独自のイベント引数クラスを定義する。

Controls/ItemControl.xaml.cs
/// <summary>
/// 付箋紙が削除される時のイベントのイベント引数クラス
/// </summary>
[DebuggerStepThrough]
public class ItemDeletingEventArgs : RoutedEventArgs {
   private string id;

   /// <summary>
   /// 削除される付箋紙のIdを取得します。
   /// </summary>
   public string Id {
       get { return id; }
   }

   /// <summary>
   /// 削除される付箋紙のIdを設定するコンストラクタ
   /// </summary>
   /// <param name="id">付箋紙のId</param>
   public ItemDeletingEventArgs(string id) {
       this.id = id;
   }
}

RoutedEventArgsクラスを継承して、Idプロパティを定義している。Idプロパティには削除する付箋紙のIdを渡す。

そして、このイベント引数クラスを引数にとるイベントをItemControlクラスに定義する。

Controls/ItemControl.xaml.cs
/// <summary>
/// 付箋紙が削除される時に呼び出されます。
/// </summary>
public event EventHandler<ItemDeletingEventArgs> ItemDeleting;
/// <summary>
/// ItemDeletingイベントを呼び出します。
/// </summary>
/// <param name="e">イベント引数</param>
protected virtual void OnItemDeleting(ItemDeletingEventArgs e) {
   if(ItemDeleting != null) ItemDeleting(this, e);
}

ItemDeletingイベントとそれを内部から呼び出すためのOnItemDeletingメソッドを定義した。

そして、「x」ボタンがクリックされた時のイベントハンドラでItemDeletingイベントを呼び出す。

Controls/ItemControl.xaml.cs
private void deleteButton_Click(object sender, RoutedEventArgs e) {
   var item = (Item)DataContext;

   OnItemDeleting(new ItemDeletingEventArgs(item.Id) {
       Source = this
   });
}

あとはメイン画面側でこのイベントに対応して、付箋紙を削除してやればいい。

Page.xaml.cs

using System;
using System.Net;
using System.Xml.Linq;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Browser;
using System.Windows.Controls;

using Fusen.Controls;

namespace Fusen {
   public partial class Page : UserControl {
       private string rootUrl = "http://localhost:1100/";
       /// <summary>
       /// 前回クリックされた時間
       /// </summary>
       private DateTime lastClickTime = DateTime.MinValue;

       public Page() {
           InitializeComponent();
       }

       /// <summary>
       /// 付箋紙の一覧を読み込みます。
       /// </summary>
       private void LoadItems() {
           var webClient = new WebClient();
           webClient.DownloadStringCompleted += (s, e) => {
               var xml = XDocument.Parse(e.Result);
               var result = from item in xml.Descendants("item")
                            select new Item {
                                Id = (string)item.Element("id"),
                                Left = (double)item.Element("left"),
                                Top = (double)item.Element("top"),
                                Width = (double)item.Element("width"),
                                Height = (double)item.Element("height"),
                                Comment = (string)item.Element("comment"),
                                Color = (string)item.Element("color")
                            };

               foreach(var item in result) LayoutRoot.Children.Add(CreateItemControl(item));
           };
           webClient.DownloadStringAsync(new Uri(rootUrl + "Item/List.aspx"));
       }

       /// <summary>
       /// 指定した位置に新しく付箋紙を追加します。
       /// </summary>
       /// <param name="location">追加する位置</param>
       private void NewItem(Point location) {
           var item = new Item {
               Left = location.X, Top = location.Y,
               Width = 200, Height = 150,
               Color = "Yellow"
           };
           var webClient = new WebClient();
           webClient.DownloadStringCompleted += (s, e) => {
               if(e.Error != null) {
                   HtmlPage.Window.Alert("付箋紙の追加に失敗しました。");

               } else {
                   item.Id = e.Result;

                   LayoutRoot.Children.Add(CreateItemControl(item));
               }
           };
           webClient.DownloadStringAsync(new Uri(rootUrl +
               string.Format("Item/New.aspx?left={0}&top={1}&width={2}&height={3}&color={4}",
                   item.Left, item.Top, item.Width, item.Height, item.Color
               )
           ));
       }

       /// <summary>
       /// 指定したidの付箋紙を削除します。
       /// </summary>
       /// <param name="itemCtrl">付箋紙コントロール</param>
       /// <param name="id">付箋紙のid</param>
       private void DeleteItem(ItemControl itemCtrl, string id) {
           var webClient = new WebClient();
           webClient.DownloadStringCompleted += (s, e) => {
               if(e.Error != null) {
                   HtmlPage.Window.Alert("付箋紙の削除に失敗しました。");

               } else {
                   LayoutRoot.Children.Remove(itemCtrl);
               }
           };
           webClient.DownloadStringAsync(new Uri(
               string.Format(rootUrl + "Item/Delete.aspx?id={0}", id)
           ));
       }

       /// <summary>
       /// 指定した付箋紙情報から付箋紙コントロールを生成します。
       /// </summary>
       /// <param name="item">付箋紙情報</param>
       /// <returns>付箋紙コントロール</returns>
       private ItemControl CreateItemControl(Item item) {
           var itemCtrl = new ItemControl {
               DataContext = item
           };
           itemCtrl.ItemDeleting += itemCtrl_ItemDeleting;

           return itemCtrl;
       }

       private void UserControl_Loaded(object sender, RoutedEventArgs e) { LoadItems(); }

       private void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
           var now = DateTime.Now;
           // ダブルクリックされた!!
           if((now - lastClickTime).TotalMilliseconds <= 200) {
               NewItem(e.GetPosition(this));
           }
           lastClickTime = now;
       }

       private void itemCtrl_ItemDeleting(object sender, ItemDeletingEventArgs e) {
           if(HtmlPage.Window.Confirm("付箋紙を削除しますか?")) {
               DeleteItem((ItemControl)sender, e.Id);
           }
       }
   }
}

ItemControlのインスタンス化をCreateItemControlというメソッドにやらせるようにした。ここでItemControlに対してイベントハンドラの追加とかをやらせている。

これでビルドして実行すると、付箋紙の「x」ボタンを押して削除確認メッセージに「OK」をすると付箋紙が削除されるはず。

これで追加と削除ができたので、残るは編集のみ。これはいろいろとややこしいので次回にする。

ここまでのソース

OneDrive

*1:単純に定数に分離すればいいという問題でもない