2010年2月9日火曜日

第13回 オプションを保存する

今回はちょっと中途半端な出来で終わるかもしれませんが、
作ってから書くまであんまり期間が空くと自分で忘れちゃうので。

まずオプションとしてこれまでUserInfoクラスくらいしかありませんでしたが、
Optionクラス、TimelineSettingクラス、UserInfoクラスに階層化します。
TimelineSettingクラスはタイムラインのURL、更新間隔などを持ちます。

ファイルへの保存はXmlSerializerを使うとすごく簡単です。
逆に凝ったことはできないかもしれません。
私はここらへんを参照させてもらいました。
http://dobon.net/vb/dotnet/file/xmlserializer.html



 public class Option {
  public Option() {
  }

  #region Property
  public UserInfo UserInfo { get; set; }
  [XmlArrayItem(typeof(TimelineSetting))]
  public List<TimelineSetting> TimelineSettings { get; set; }
  #endregion

  #region Public Method
  /// <summary>
  /// ファイルがないときなどにデフォルト値を読み込む
  /// </summary>
  /// <returns></returns>
  static private Option LoadDefault() {
・・省略
  }

  /// <summary>
  /// Optionの内容をファイルに保存する
  /// </summary>
  /// <param name="filename">保存ファイル名</param>
  public void Save(string filename) {
   var serializer = new XmlSerializer(this.GetType());
   using (var stream = new FileStream(filename, FileMode.Create)) {
    serializer.Serialize(stream, this);
   }
  }

  /// <summary>
  /// Optionの内容をファイルに保存する。ファイル名はLoad()時に使用したもの。
  /// </summary>
  public void Save() {
   Debug.Assert(!string.IsNullOrEmpty(this._filename));
   this.Save(this._filename);
  }

  /// <summary>
  /// ファイルからOptionクラスを読み込む
  /// </summary>
  /// <param name="filename">読み込むファイル名</param>
  /// <returns>読み込んだオブジェクト</returns>
  static public Option Load(string filename) {
   //ファイルがなかったらデフォルト値を読み込む
   if (!File.Exists(filename)) {
    var newInstance = LoadDefault();
    newInstance._filename = filename;
    return newInstance;
   }

   //読み込み
   var serializer = new XmlSerializer(typeof(Option));
   Option instance;
   using (var stream = new FileStream(filename, FileMode.Open)) {
    instance = serializer.Deserialize(stream) as Option;
   }
   //Save()の時のためにファイル名を保存
   instance._filename = filename;
   return instance;
  }
  #endregion Public Method

  #region private member
  private string _filename;
  #endregion
 }

TimelineSettingクラスはフィールドを持つだけです。


 public class TimelineSetting {
  /// <summary>
  /// タイムラインに付ける名前
  /// </summary>
  public string Name { get; set; }
  /// <summary>
  /// タイムラインのURL
  /// </summary>
  public string Url { get; set;}
  /// <summary>
  /// タイムラインのリクエストに認証が必要かどうか
  /// </summary>
  public bool IsAuthRequired { get; set; }
  /// <summary>
  /// 自動更新する間隔
  /// </summary>
  public int UpdateInterval { get; set; }
 }



 public class UserInfo {
  [XmlIgnore]
  public string Password { get; set; }//これは保存しない

  /// <summary>
  /// ユーザーID
  /// </summary>
  public string UserName { get; set; }

  /// <summary>
  /// ファイル保存用にエンコードしたパスワード
  /// </summary>
  public string PasswordEncoded {
   get {
    return Convert.ToBase64String(Encoding.ASCII.GetBytes(Password));
   }
   set {
    Password = Encoding.ASCII.GetString(Convert.FromBase64String(value));
   }
  }

  /// <summary>
  /// ユーザー情報が有効かどうか
  /// </summary>
  [XmlIgnore]
  public bool IsValid {
   get {
    //とりあえずの実装
    if ( string.IsNullOrEmpty(UserName) || string.IsNullOrEmpty(Password)) {
     return false;
    }
    return true;
   }
  }
 }


ローカルといえど、さすがにパスワードをプレーンな状態で保存するのはマズイかなと思い、
簡単にEncode(変換)して保存します。
これはMiniTwitterの実装を参考にさせてもらいました。

そのうちOAuth認証に対応しないといけないので(TwitterのAPIがそうなるらしい)、
そのときにはIDとパスワードをローカルに保存しないようにします。

ローカルにパスワードを保存する場合で、本格的にセキュリティに気をつけなければならない時は
他の暗号化方式を使ったり、
プログラム内でもSecureStringクラスで保持した方がいいかもしれません。


あとIsValidでIDとパスワードを検証するロジックを入れていますが、
データ構造をそのままXMLに保存するクラスは本当にデータだけにして
ロジックは別のところにおいた方がわかりやすいかもしれません。

ダイアログ画面のxamlは以下のようになります。
DataContextにはOptionクラスはコンストラクタで入るようにしています。



<Window x:Class="WTwitter.View.OptionDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="オプション設定" Width="300" Height="300" Loaded="Window_Loaded">
 <Window.BindingGroup>
  <BindingGroup/>
 </Window.BindingGroup>
 <DockPanel>
  <Button IsDefault="True" Click="OkButton_Click"
    DockPanel.Dock="Bottom" HorizontalAlignment="Right">OK(_O)</Button>
  <TabControl>
   <TabItem Header="ID">
    <Grid>
     <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition Width="Auto"/>
     </Grid.ColumnDefinitions>
     <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
     </Grid.RowDefinitions>
     <TextBlock Text="UserName" Grid.Row="0" Grid.Column="0"/>
     <TextBox Grid.Row="0" Text="{Binding Path=UserInfo.UserName}"
        Grid.Column="1" Width="100"/>
     
     <TextBlock Text="Password" Grid.Row="1" Grid.Column="0"/>
     <TextBox Text="{Binding Path=UserInfo.Password}"
      Grid.Row="1" Grid.Column="1" Width="100"/>

    </Grid>
   </TabItem>
   
   <TabItem Header="Timeline">
    <ItemsControl ItemsSource="{Binding Path=TimelineSettings}" HorizontalContentAlignment="Stretch">
     <ItemsControl.ItemTemplate>
      <DataTemplate>
       <StackPanel>
        <DockPanel>
         <TextBlock DockPanel.Dock="Left">Name</TextBlock>
         <TextBox Text="{Binding Path=Name}" DockPanel.Dock="Left"/>
         <CheckBox Content="Auth" IsChecked="{Binding Path=IsAuthRequired}"/>
        </DockPanel>
        <DockPanel>
         <TextBlock DockPanel.Dock="Left">URL</TextBlock>
         <TextBox Text="{Binding Path=Url}"/>
        </DockPanel>
       </StackPanel>
      </DataTemplate>
     </ItemsControl.ItemTemplate>
    </ItemsControl>
   </TabItem>
  </TabControl>
 </DockPanel>
</Window>



主な変更点は3つ
・全体をTabでページ分け
・設定関係のクラスを階層化したのでBindingのパスが1段階深くなっている
・OKボタンを押したときにOptionクラスのSave()を呼び出すようにしている
例:Binding Path=UserInfo.UserName

ちなみに、タイムラインの追加・削除をまだ作ってないので、
デフォルトである2つのタイムラインのURLなどの変更しかできません。
(1回保存した後でできるXMLファイルを直接編集すれば3つ以上にできると思いますが。)

保存できるようになったので、起動時にファイルから設定を読み出すように変更します。



 public partial class App : Application {

  protected override void OnStartup(StartupEventArgs e) {
   base.OnStartup(e);

   var option = Option.Load("Setting.xml");

   var viewModel = new MainWindowViewModel(option);
   viewModel.Initialize();

   var mainWindow = new MainWindow(viewModel);
   mainWindow.DataContext = viewModel;
   mainWindow.Show();
  }
 }


ちなみにMainWindowをnewするまえにviewModelをInitialize()しないと、
配置がおかしくなるみたいです。
Windowをnewしたときに配置を計算するみたいですね。(show()ではなくて)
なのでnewする時点でViewModelのアイテムを確定させておいたほうがいいみたいです。
showしたあとのアイテムの追加はうまく計算してくれません。
Windowのサイズを変更すると修正されるみたいですが。。

あとは初回起動時はユーザーIDとパスワードが入っていないのでそのままだと必ず失敗するので、
MainWindowViewModelのInitializeでタイムラインが取得できる状態かどうかを調べて
可能な場合のみタイマを作動させています(vm.IsUpdatableの部分)



  public void Initialize() {
   ////Modelの作成

   //ツールバーのボタンの作成
   var baseUri = System.IO.Directory.GetParent(
    System.Reflection.Assembly.GetExecutingAssembly().Location).FullName;
   var commands = new ImageButton[] {
    new ImageButton("設定", OptionDialogCommand,
     baseUri + @"\Icon\spanner_48.png")
   };

   foreach (var comm in commands) {
    _commandButtons.Add(comm);
   }

   //Modelの作成
   foreach (var setting in _option.TimelineSettings) {
    TimelineViewModel vm;
    if (setting.IsAuthRequired) {
     vm = new TimelineViewModel(setting.Name,
       new Timeline(setting.Url, _userInfo));
    } else {
     vm = new TimelineViewModel(setting.Name, new Timeline(setting.Url));
    }
    Timelines.Add(vm);
    if (vm.IsUpdatable) {
     vm.Update();
     vm.StartAutoUpdate();
    }
   }

   _submitPanel = new SubmitPanelViewModel(_userInfo);
   OnPropertyChanged("SubmitPanel");
  }


オプションを変更した場合は再起動しないと反映されませんが。。。今回はここまで。

ソースコードはこちら

http://wtwitter.codeplex.com/SourceControl/changeset/view/40836

1 件のコメント:

  1. 書き忘れていましたが、通常こういったオプション保存などは
    標準で用意されているSettingsクラスの機能を使うのが推奨されているようです。
    Settingsを使うとより簡単ですが、設定はユーザーデーターフォルダに保存されます。
    ですが、私は設定はアプリと同じフォルダに保存したい派なので
    今回のような方法を使っています。

    返信削除