作ってから書くまであんまり期間が空くと自分で忘れちゃうので。
まずオプションとしてこれまで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
書き忘れていましたが、通常こういったオプション保存などは
返信削除標準で用意されているSettingsクラスの機能を使うのが推奨されているようです。
Settingsを使うとより簡単ですが、設定はユーザーデーターフォルダに保存されます。
ですが、私は設定はアプリと同じフォルダに保存したい派なので
今回のような方法を使っています。