ラベル C# の投稿を表示しています。 すべての投稿を表示
ラベル C# の投稿を表示しています。 すべての投稿を表示

2014年9月11日木曜日

[WPF][Silverlight][C#] ScrollViewer でマウスでの横スクロールと慣性スクロール

将来的に横スクロールにしたいという話があって、事前調査してみました。
やりたいことは下記3つ。
  1. マウスホイールで横スクロール
  2. ドラッグで横スクロール
  3. 慣性スクロール
あんまり情報ないですねー。とりあえずざくっとと作ってみたので、コードだけ紹介します。WPF版では ScrollViewer のカスタムコントロールで、Silverlight では ScrollViewer  を継承できないので Behavior で作りました。ほぼ同じようなコードになってます。加速度までは考慮していないのであしからず。

WPF版(Custom Control)
    /// <summary>
    /// 慣性スクロールビューア
    /// </summary>
    public class MomentumScrollViewer : ScrollViewer
    {
        #region インスタンスフィールド
        /// <summary>
        /// マウスオフセット位置
        /// </summary>
        private double mouseOffset;
        /// <summary>
        /// 開始位置
        /// </summary>
        private double startOffset;
        /// <summary>
        /// プレス有無
        /// </summary>
        private bool isPressed = false;
        /// <summary>
        /// 最終位置
        /// </summary>
        private double lastPosition;
        /// <summary>
        /// 移動量
        /// </summary>
        private double momentumValue;
        #endregion
        #region 依存プロパティ
        /// <summary>
        /// 運動量を取得または設定します。
        /// </summary>
        public double MomentumValue
        {
            get { return (double)GetValue(MomentumValueProperty); }
            set { SetValue(MomentumValueProperty, value); }
        }
        /// <summary>
        /// 運動量依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty MomentumValueProperty =
            DependencyProperty.Register("MomentumValue", typeof(double), typeof(MomentumScrollViewer), new PropertyMetadata(0.5));
        /// <summary>
        /// 水平位置を取得または設定します。
        /// </summary>
        public double HorizontalPosition
        {
            get { return (double)GetValue(HorizontalPositionProperty); }
            set { SetValue(HorizontalPositionProperty, value); }
        }
        /// <summary>
        /// 水平位置依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty HorizontalPositionProperty =
            DependencyProperty.Register("HorizontalPosition", typeof(double), typeof(MomentumScrollViewer), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnHorizontalPositionChanged)));
        #endregion
        #region コンストラクタ
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MomentumScrollViewer()
        {
            // 各イベント登録
            this.PreviewMouseWheel += MomentumScrollViewer_PreviewMouseWheel;
            this.Loaded += (sender, e) =>
                {
                    var content = this.GetValue(ScrollViewer.ContentProperty) as FrameworkElement;
                    if (content != null)
                    {
                        content.MouseLeftButtonDown += content_MouseLeftButtonDown;
                        content.MouseLeftButtonUp += content_MouseLeftButtonUp;
                        content.MouseMove += content_MouseMove;
                        content.MouseLeave += content_MouseLeave;
                    }
                };
        }
        #endregion
        #region イベント
        /// <summary>
        /// 水平スクロール位置変更イベント
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="e"></param>
        private static void OnHorizontalPositionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var scrollViewer = obj as MomentumScrollViewer;
            if (scrollViewer != null)
            {
                scrollViewer.ScrollToHorizontalOffset((double)e.NewValue);
            }
        }
        #region ホイールでのスクロール
        /// <summary>
        /// マウスホイールイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MomentumScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            beginScroll(e.Delta);
            e.Handled = true;
        }
        #endregion
        #region ドラッグによるスクロール
        /// <summary>
        /// 左ボタンダウンイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var position = e.GetPosition(this);
            this.mouseOffset = position.X;
            this.startOffset = this.HorizontalOffset;
            this.isPressed = true;
        }
        /// <summary>
        /// マウス移動イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseMove(object sender, MouseEventArgs e)
        {
            if (this.isPressed)
            {
                var position = e.GetPosition(this);
                // 前回値からの運動量を保持し、前回値を更新
                momentumValue = lastPosition - position.X;
                lastPosition = position.X;
                // 変化量を算出し、スクロール
                var delta = (position.X > mouseOffset) ? -(position.X - mouseOffset) : mouseOffset - position.X;
                this.ScrollToHorizontalOffset(startOffset + delta);
            }
        }
        /// <summary>
        /// マウスリーブイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseLeave(object sender, MouseEventArgs e)
        {
            dragEnd();
        }
        /// <summary>
        /// 左ボタンアップイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            dragEnd();
        }
        #endregion
        #endregion
        #region プライベートメソッド
        /// <summary>
        /// ドラッグ終了処理
        /// </summary>
        private void dragEnd()
        {
            if (this.isPressed)
            {
                this.isPressed = false;
                // 運動量がある場合は慣性スクロール
                if (momentumValue != 0)
                {
                    beginScroll(momentumValue * (-1));
                }
            }
        }

        /// <summary>
        /// 慣性スクロール開始
        /// </summary>
        /// <param name="delta">変化量</param>
        private void beginScroll(double delta)
        {
            // 目的地設定
            var to = HorizontalOffset - delta * MomentumValue;
            if (to < 0)
                to = 0;
            if (to > ExtentWidth)
                to = ExtentWidth;
            // 慣性スクロールアニメーション開始
            var animation = new DoubleAnimation(to, new Duration(TimeSpan.FromMilliseconds(1000)));
            animation.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut };
            this.BeginAnimation(MomentumScrollViewer.HorizontalPositionProperty, animation);
        }
        #endregion
    }


Sivlerlight版(Behavior)
    /// <summary>
    /// 慣性スクロールビヘイビア
    /// </summary>
    public class MomentumScrollingBehavior : Behavior<ScrollViewer>
    {
        #region インスタンスフィールド
        /// <summary>
        /// マウスオフセット位置
        /// </summary>
        private double mouseOffset;
        /// <summary>
        /// 開始位置
        /// </summary>
        private double startOffset;
        /// <summary>
        /// プレス有無
        /// </summary>
        private bool isPressed = false;
        /// <summary>
        /// 最終位置
        /// </summary>
        private double lastPosition;
        /// <summary>
        /// 移動量
        /// </summary>
        private double momentumValue;
        #endregion
        #region 依存プロパティ
        /// <summary>
        /// 運動量を取得または設定します。
        /// </summary>
        public double MomentumValue
        {
            get { return (double)GetValue(MomentumValueProperty); }
            set { SetValue(MomentumValueProperty, value); }
        }
        /// <summary>
        /// 運動量依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty MomentumValueProperty =
            DependencyProperty.Register("MomentumValue", typeof(double), typeof(MomentumScrollingBehavior), new PropertyMetadata(0.5));
        /// <summary>
        /// 水平位置を取得または設定します。
        /// </summary>
        public double HorizontalPosition
        {
            get { return (double)GetValue(HorizontalPositionProperty); }
            set { SetValue(HorizontalPositionProperty, value); }
        }
        /// <summary>
        /// 水平位置依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty HorizontalPositionProperty =
            DependencyProperty.Register("HorizontalPosition", typeof(double), typeof(MomentumScrollingBehavior), new PropertyMetadata(0.0, new PropertyChangedCallback(OnHorizontalPositionChanged)));
        #endregion

        #region イベント
        /// <summary>
        /// アタッチ完了
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.MouseWheel += scrollViewer_MouseWheel;
            this.AssociatedObject.Loaded += (sender, e) =>
            {
                var content = this.AssociatedObject.GetValue(ScrollViewer.ContentProperty) as FrameworkElement;
                if (content != null)
                {
                    content.MouseLeftButtonDown += content_MouseLeftButtonDown;
                    content.MouseLeftButtonUp += content_MouseLeftButtonUp;
                    content.MouseMove += content_MouseMove;
                    content.MouseLeave += content_MouseLeave;
                }
            };
        }
        /// <summary>
        /// デタッチ
        /// </summary>
        protected override void OnDetaching()
        {
            this.AssociatedObject.MouseWheel -= scrollViewer_MouseWheel;
            var content = this.AssociatedObject.GetValue(ScrollViewer.ContentProperty) as FrameworkElement;
            if (content != null)
            {
                content.MouseLeftButtonDown -= content_MouseLeftButtonDown;
                content.MouseLeftButtonUp -= content_MouseLeftButtonUp;
                content.MouseMove -= content_MouseMove;
                content.MouseLeave -= content_MouseLeave;
            }
            base.OnDetaching();
        }
        /// <summary>
        /// 水平スクロール位置変更イベント
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="e"></param>
        private static void OnHorizontalPositionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var behavior = obj as MomentumScrollingBehavior;
            if (behavior != null)
            {
                behavior.AssociatedObject.ScrollToHorizontalOffset((double)e.NewValue);
            }
        }
        /// <summary>
        /// マウスホイールイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void scrollViewer_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            beginScroll(e.Delta);
            e.Handled = true;
        }
        #region ドラッグによるスクロール
        /// <summary>
        /// 左ボタンダウンイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var position = e.GetPosition(this.AssociatedObject);
            this.mouseOffset = position.X;
            this.startOffset = this.AssociatedObject.HorizontalOffset;
            this.isPressed = true;
        }
        /// <summary>
        /// マウス移動イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseMove(object sender, MouseEventArgs e)
        {
            if (this.isPressed)
            {
                var position = e.GetPosition(this.AssociatedObject);
                // 前回値からの運動量を保持し、前回値を更新
                momentumValue = lastPosition - position.X;
                lastPosition = position.X;
                // 変化量を算出し、スクロール
                var delta = (position.X > mouseOffset) ? -(position.X - mouseOffset) : mouseOffset - position.X;
                this.AssociatedObject.ScrollToHorizontalOffset(startOffset + delta);
            }
        }
        /// <summary>
        /// マウスリーブイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseLeave(object sender, MouseEventArgs e)
        {
            dragEnd();
        }
        /// <summary>
        /// 左ボタンアップイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            dragEnd();
        }
        #endregion
        #endregion
        #region プライベートメソッド
        /// <summary>
        /// ドラッグ終了処理
        /// </summary>
        private void dragEnd()
        {
            if (this.isPressed)
            {
                this.isPressed = false;
                // 運動量がある場合は慣性スクロール
                if (momentumValue != 0)
                {
                    beginScroll(momentumValue * (-1));
                }
            }
        }

        /// <summary>
        /// 慣性スクロール開始
        /// </summary>
        /// <param name="delta">変化量</param>
        private void beginScroll(double delta)
        {
            // 目的地設定
            var to = this.AssociatedObject.HorizontalOffset - delta * MomentumValue;
            if (to < 0)
                to = 0;
            if (to > this.AssociatedObject.ExtentWidth)
                to = this.AssociatedObject.ExtentWidth;
            // 慣性スクロールアニメーション開始
            var storyboard = new Storyboard();
            storyboard.Children.Add(new DoubleAnimation()
            {
                From = this.AssociatedObject.HorizontalOffset,
                To = to,
                Duration = new Duration(TimeSpan.FromMilliseconds(1000)),
                EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut }
            });
            Storyboard.SetTarget(storyboard, this);
            Storyboard.SetTargetProperty(storyboard, new PropertyPath("HorizontalPosition"));
            storyboard.Begin();
        }
        #endregion
    }










2014年9月2日火曜日

[C#][SQL Server Express LocalDB] キー 'attachdbfilename' には無効な値です。

SQL Server Express CE(Compact Edition)が廃止されて、代わりにSQL Server LocalDB という SQL Server の機能がフルで使えるデータベースが使えるようになりました。前者はインプロセスでしたし、SQL Serverはサービスとして動作しますが、LocalDBは使用時にプロセスが起動し、使わなくなったらプロセスが終了します。これは便利だと思い、ちょうど以前 SQL Server CE を使用したアプリケーションのリメイクを行うこととなったので、ついでに LocalDB に移行しました。
順調に試験が終わってリリースしたところ、実機でエラーが出て起動できないと報告がありました。調べてみると次のエラーが出ていました。

キー 'attachdbfilename' には無効な値です。
   場所 System.Data.SqlClient.SqlConnectionString.VerifyLocalHostAndFixup(String& host, Boolean enforceLocalHost, Boolean fixup)
   場所 System.Data.SqlClient.SqlConnectionString..ctor(String connectionString)
   場所 System.Data.SqlClient.SqlConnectionFactory.CreateConnectionOptions(String connectionString, DbConnectionOptions previous)
   場所 System.Data.ProviderBase.DbConnectionFactory.GetConnectionPoolGroup(String connectionString, DbConnectionPoolGroupOptions poolOptions, DbConnectionOptions& userConnectionOptions)
   場所 System.Data.SqlClient.SqlConnection.ConnectionString_Set(String value)
   場所 System.Data.SqlClient.SqlConnection.set_ConnectionString(String value)
   場所 System.Data.SqlClient.SqlConnection..ctor(String connectionString)

うぬぬ、よくわからんです。海外のサイトとかも含めて調べていたら、どうもバージョンが不一致だからダメだっていうことらしいです。実機には「Microsoft SQL Server 2014 Express のダウンロード 」をインストールしています。Visual Studio の 2013 で LocalDB を追加したら、LocalDB の は v11.0 で生成されますが、実機にインストールした LocalDB は v12.0 なんだとか。Visual Studio の最新のサービスパックを適用しても v11.0 で生成されてしまう。どうしろというのだ。まったく。

苦肉の策ですが、開発環境は v11.0 だけどインスタンス名を v12.0 にしてしまって接続することで回避できました。アプリケーション起動時に下記コマンドを実行します。


    // V12インスタンスのインストール
    var process = new Process()
    {
        StartInfo = new ProcessStartInfo(System.Environment.GetEnvironmentVariable("ComSpec"))
                    {
                        Arguments = "/c sqllocaldb create v12.0",
                        CreateNoWindow = true,
                        RedirectStandardOutput = true,
                        RedirectStandardInput = false,
                        UseShellExecute = false,
                    },
    };
    process.Start();
    var result = process.StandardOutput.ReadToEnd();
    process.WaitForExit();
    process.Close();
    if(!result.Contains("v12.0"))
    {
        throw new Exception(result);
    }

接続文字列(ConnectionString)を次のように変更します。

Data Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\Database1.mdf;Integrated Security=True
                       ↓
Data Source=(LocalDB)\v12.0;AttachDbFilename=|DataDirectory|\Database1.mdf;Integrated Security=True


これで開発機でも実機でもうまく動作するようになりました。




2014年7月31日木曜日

[WPF][Silverlight][C#]MVVM パターンで TextBox をフォーカス

FocusManager を使えばフォーカス設定できますが、TextBox の IsEnabled が false だったり、複数のFocusManager の設定があると、意図した動きをしてくれなくなります。

如何にすればシンプルに出来るか考えてみましたが、Behavior 使うのが一番シンプルに収まりそうです。フォーカス設定のみだと寂しいので、文字列全選択も書きました。



    public class TextBoxBehavior : Behavior<TextBox>
    {
        /// <summary>
        /// フォーカス設定
        /// </summary>
        public void Focus()
        {
            this.AssociatedObject.Focus();
        }

        /// <summary>
        /// 全選択
        /// </summary>
        public void SelectAll()
        {
            this.AssociatedObject.SelectAll();
        }
    }

後は、XAML側でボタンクリックされた時などに、CallMethodAction を書くだけでOKです。

<Window x:Class="Sample.TestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:local="clr-namespace:Sample">
    <StackPanel>
        <Button Content="Button" HorizontalAlignment="Left" Width="75">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <ei:CallMethodAction TargetObject="{Binding ElementName=textBoxBehavior}" MethodName="Focus" />
                    <ei:CallMethodAction TargetObject="{Binding ElementName=textBoxBehavior}" MethodName="SelectAll" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>

        <TextBox HorizontalAlignment="Left" Height="23" Text="TextBox" Width="120">
            <i:Interaction.Behaviors>
                <local:TextBoxBehavior x:Name="textBoxBehavior" />
            </i:Interaction.Behaviors>
        </TextBox>
    </StackPanel>
</Window>





2014年4月24日木曜日

[WPF][C#]XMLデータをTreeViewで編集

XML データを TreeView に表示できるところまでやりました。次は編集ですね。

編集画面追加
TreeView 上で編集するのは難しそうなので、編集エリアを設けて選択した要素を操作するのがよさそうです(MVVMパターンとかバインドとか面倒だったので、そちらは割愛させて頂きます。)。そして、ごめんなさい、色々と面倒くさくなってきたので、一気に貼り付けます。


[XAML]
    <Window.Resources>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsExpanded" Value="True" />
        </Style>
        <HierarchicalDataTemplate x:Key="XmlTemplate" ItemsSource="{Binding Path=Elements}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Name="nameTextBlock" Text="{Binding Path=Name.LocalName}"/>
                <TextBlock Name="valueTextBlock" FontWeight="Bold" Margin="5,0" />
            </StackPanel>
            <HierarchicalDataTemplate.Triggers>
                <DataTrigger Binding="{Binding Path=HasElements}" Value="False">
                    <Setter TargetName="valueTextBlock" Property="Text" Value="{Binding Path=Value}" />
                </DataTrigger>
            </HierarchicalDataTemplate.Triggers>
        </HierarchicalDataTemplate>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="300"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <!--表示エリア-->
        <TreeView Name="treeView" Margin="5"
                  ItemTemplate= "{StaticResource XmlTemplate}"
                  SelectedItemChanged="treeView_SelectedItemChanged" 
                  >
        </TreeView>
        
        <!--編集エリア-->
        <StackPanel Grid.Column="1" >
            <GroupBox Header="要素名" Margin="5">
                <StackPanel Orientation="Horizontal" Margin="0,5">
                    <TextBox Name="nameTextBox" Text="{Binding Path=Name.LocalName}" Width="80" />
                    <Button Content="追加" Name="addElementButton" Click="addElementButton_Click"  Width="50" Margin="5,0,0,0"/>
                    <Button Content="更新" Name="updateNameButton" Click="updateNameButton_Click"  Width="50" Margin="5,0,0,0"/>
                    <Button Content="削除" Name="delElementButton" Click="delElementButton_Click"  Width="50" Margin="5,0,0,0"/>
                </StackPanel>
            </GroupBox>

            <GroupBox Header="" Margin="5" Name="valueGroup">
                <StackPanel Orientation="Horizontal" Margin="0,5">
                    <TextBox Name="valueTextBox" Text="{Binding Path=Value}" Width="80" />
                    <Button Content="更新" Name="updateValueButton" Click="updateValueButton_Click"  Width="50" Margin="5,0,0,0"/>
                </StackPanel>
            </GroupBox>
        </StackPanel>

    </Grid>


[Code]
        /// <summary>
        /// 要素選択変更イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            var element = e.NewValue as XElement;
            if (element != null)
            {
                nameTextBox.Text = element.Name.LocalName;
                if (!element.HasElements)
                {
                    // 子要素が無ければ値を編集できるようにする
                    valueTextBox.Text = element.Value;
                    valueGroup.Header = element.Name.LocalName;
                    valueGroup.IsEnabled = true;
                }
                else
                {
                    valueTextBox.Text = "";
                    valueGroup.Header = "";
                    valueGroup.IsEnabled = false;
                }
            }
        }

        /// <summary>
        /// 要素追加ボタン実行
        /// </summary>
        /// <remarks>
        /// 選択中要素に子要素を追加する
        /// </remarks>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void addElementButton_Click(object sender, RoutedEventArgs e)
        {
            var element = treeView.SelectedItem as XElement;
            if (!string.IsNullOrEmpty(nameTextBox.Text) && element != null)
            {
                XNamespace x = element.Name.Namespace;
                var newElement = new XElement(x + nameTextBox.Text) { Value = valueTextBox.Text };
                element.Add(newElement);
            }
        }

        /// <summary>
        /// 要素更新ボタン実行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void updateNameButton_Click(object sender, RoutedEventArgs e)
        {
            var element = treeView.SelectedItem as XElement;
            if (!string.IsNullOrEmpty(nameTextBox.Text) && element != null)
            {
                XNamespace x = element.Name.Namespace;
                element.Name = x + nameTextBox.Text;

                // ①子要素の有無をチェックしないと消えてしまう
                element.Value = valueTextBox.Text;

                // ②選択している要素が解除されてしまう
                treeView.Items.Refresh();
            }
        }

        /// <summary>
        /// 要素削除ボタン実行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void delElementButton_Click(object sender, RoutedEventArgs e)
        {
            var element = treeView.SelectedItem as XElement;
            if (element != null)
            {
                element.Remove();
            }
        }

        /// <summary>
        /// 値更新ボタン実行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void updateValueButton_Click(object sender, RoutedEventArgs e)
        {
            var element = treeView.SelectedItem as XElement;
            if (!string.IsNullOrEmpty(nameTextBox.Text) && element != null)
            {
                element.Value = valueTextBox.Text;
            }
        }


残件
残件とか課題としては、MVVM化とかコメントの①②があります。あと属性の編集もですかね。これは要素選択変更イベントで、属性があれば、名称と値のエンティティのリストを作成し、DataGridとかにバインドさせて編集後、更新ボタンとかで要素の属性情報を全て更新すればいいかな。