Bell’s diary

WPFやC#の小ネタ、その他いろいろ気まぐれで投稿してみる予定。

LINQtoSQLで、主キーの変更をする

LINQtoSQLでは、主キーのフィールドを変更してSubmitChanges()しようとすると例外が発生してしまい、変更することができません。

代わりに、元々のレコードは削除して、代わりに、主キー以外の全てのフィールドをコピーして(DBの値でなく、画面で主キー以外のいろいろなフィールドも編集した結果からコピー)主キーだけを新しいものにしたレコードを追加する、という組み合わせで、一応、見かけ上は主キーを変更したのと同等の変更をすることができます。(厳密にはすでに書いている通り、旧レコードの削除+新レコードの追加ですが)。

これを行うコード例です。Dbは、LINQtoSQLで作成されるDataContextのインスタンスが入っており、同一アセンブリ内のBellという名前空間に、作りたいエンティティのクラスが定義されていて、その主キーは「Id」という名前の列であり、画面に呼び出して編集対象にしてるテーブル名がTargetTableNameに入ってる、という前提のコードです(前提多くてすみません)。

もともと、複数のコード系マスタテーブルを編集するために作ったもので、Idという名前のフィールドが存在し、それが主キーになっているテーブルならどれでも対応できるよう、途中でdynamicで受けています。また、複数ある対象の中からどれを編集対象とするか、そのテーブル名をTargetTableNameに入れて呼び出すものとなっています。

なお、Id列は、「Id」という名称ですが、SQL server側の列のプロパティとしては「IDである」は「いいえ」になってる必要があります^^ 「はい」で自動で番号振られる状態だと、手動で値を設定することはできませんので。

        public void Save()
        {
            Action doAfterDelete = () => { };

            var changeSet = Db.GetChangeSet();

            for (var i = changeSet.Updates.Count-1; i >=0; i--)
            {
                var oldEntity = changeSet.Updates[i];

                var table = Db.GetTable(oldEntity.GetType());

                dynamic beforeEdit = table.GetOriginalEntityState(oldEntity);
                dynamic afterEdit = oldEntity;

                //主キーが「Id」という名前の列の場合。
                int beforeId = beforeEdit.Id;
                int afterId = afterEdit.Id;

                if (beforeId == afterId) continue;

                //ID変更の処理。LINQtoSQLでは変更はできないので、旧レコードの削除と新レコードの追加に組み替える。

                //元のレコードの削除
                table.DeleteOnSubmit(oldEntity);

                //新しいレコードの作製
                var assembly = Assembly.GetExecutingAssembly();
                dynamic newEntity = assembly.CreateInstance(
                    "Bell." + TargetTableName, false, BindingFlags.CreateInstance,
                    null, null, null, null
                    );

                //作ったレコードの全フィールドに画面で編集した値を設定
                //(IDも画面で新しいものに編集されている前提)
                foreach (var info in  oldEntity.GetType().GetProperties())
                {
                    info.SetValue(newEntity, info.GetValue(oldEntity, null));
                }

                //SubmitChangeで旧レコードを削除した後でないと新レコード追加できないのでデリゲートに追加するだけ
                doAfterDelete+=()=> { table.InsertOnSubmit(newEntity); };
            }
            //まずここで主キー変更以外の変更の更新(主キー変更する旧レコードの削除含む)
            Db.SubmitChanges();

            //主キー変更したレコードのコピーはここで追加される。
            doAfterDelete();
            Db.SubmitChanges();
        }

forループを逆順で回してるのは特に意味はないです^^

元々、更新処理を、元レコードの削除+コピーの追加に置き換えるわけだから、ChangesetのUpdatesからは該当レコードを除去した方がいいんじゃないかと思い、そのために逆順にしてコード書き始めたと思うのですが、そもそもそんなことする必要もなく無事動作したというオチ。

WPF、罫線つきのGridを作ってみる

WPFのGridは、Column、Rowを指定することで内部のコントロールを配置することができますが、この各セルを枠線、罫線で囲って区切って欲しいと言われ。

この件、過去にもいろいろな方法で試行錯誤してみたことがあったのですが、一番個人的には美しく仕上がるかなと思ったのが、罫線用の行と列を追加してしまう方法。列A、列B、列C と、実際には3列なのを、(左罫線列)、列A、(AB間の罫線列)、列B、(BC間の罫線列)、列C、(右罫線列)と、列と列の間、および全体の外側に、罫線を引くためだけの列を追加して、ここにlineなりrectangleなりで線を引いてしまうというものです。

ただこれ、行数や列数が増えると大変です。n行m列とすると、2n+1行、2m+1列まで増えますし。配置してるコントロールの座標も全部修正必要になるし、Spanを設定している場合はそれも修正が必要になる。

手作業で行や列を追加してだと、時間も手間もかかりますし、間違いも多くなりますし。だったら、自動でその修正をしてくれるカスタムコントロールを作ってしまえと思って、今日、土曜の朝に、試しに作ってみました。

using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Bell
{
    class RuledLineGrid : Grid
    {
        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);

            //罫線の太さはここで指定。
            var thickness = new GridLength(1);

            // 罫線用の行・列を追加
            var columns = ColumnDefinitions.ToArray();
            ColumnDefinitions.Clear();

            if (columns.Any())
            {
                foreach (var c in columns)
                {
                    ColumnDefinitions.Add(new ColumnDefinition {Width = thickness});
                    ColumnDefinitions.Add(c);
                }
            }
            else
            {
                ColumnDefinitions.Add(new ColumnDefinition {Width = thickness});
                ColumnDefinitions.Add(new ColumnDefinition());
            }
            ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.5) });

            var rows = RowDefinitions.ToArray();
            RowDefinitions.Clear();

            if (rows.Any())
            {
                foreach (var r in rows)
                {
                    RowDefinitions.Add(new RowDefinition {Height = thickness});
                    RowDefinitions.Add(r);
                }
            }
            else
            {
                RowDefinitions.Add(new RowDefinition {Height = thickness});
                RowDefinitions.Add(new RowDefinition());
            }
            RowDefinitions.Add(new RowDefinition { Height = thickness });

            //行・列を追加した分、Column,Row,ColumnSpan,RowSpanがずれるのでその補正再設定
            foreach (UIElement c in Children)
            {
                SetColumn(c, GetColumn(c)*2 + 1);
                SetColumnSpan(c, GetColumnSpan(c) * 2 - 1);
                SetRow(c, GetRow(c) * 2 + 1);
                SetRowSpan(c, GetRowSpan(c) * 2 - 1);
            }

            //罫線用に追加した行・列にRectangleを配置
            for (var i = 0; i < ColumnDefinitions.Count; i+=2)
            {
                var rectangle = new Rectangle() { Fill = Brushes.Black };
                Children.Add(rectangle);
                SetColumn(rectangle,i);
                SetRowSpan(rectangle,RowDefinitions.Count);
                SetZIndex(rectangle,int.MinValue);
            }
            for (var i = 0; i < RowDefinitions.Count; i+=2)
            {
                var rectangle = new Rectangle() { Fill = Brushes.Black };
                Children.Add(rectangle);
                SetRow(rectangle, i);
                SetColumnSpan(rectangle, ColumnDefinitions.Count);
                SetZIndex(rectangle,int.MinValue);
            }
        }
    }
}

使用するXAML側では、


    <local:RuledLineGrid Margin="10" Background="LightGreen">
        <local:RuledLineGrid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="50"/>
        </local:RuledLineGrid.ColumnDefinitions>
        <local:RuledLineGrid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        <local:RuledLineGrid.RowDefinitions>
    </local:RuledLineGrid>

こんな感じで通常のGridに置き換えて使えばOK。間の罫線列・罫線行は無視して、普通のGridと同じ感覚で指定します。

次のSSで、上が普通のGrid、下がRuledLineGridに置き換えたもの。

f:id:hiro19691216:20150418101508p:plain

欠点としては、結合セル(ColumnSpanまたはRowSpanが2以上のもの)に関しては、コントロールそのものが罫線の上に配置されることで罫線を隠しているので、背景が透明なTextBlockなんかを置いた場合は、背後の罫線が隠れずそのまま見えてしまうこと。

上の例のように、Backgroundに色を付けたり、透明以外にしておけばそれで解決する話なので、まぁいいかな…? という感じです。

4/20 少しコードを修正しました。ColumnDefinitionやRowDefinitionを全く定義していないGridだとうまく行かなかったので、その場合にも対応。

CollectionViewSourceのFilter条件を変更するAction

WPFのListBoxなどは、直接ObservableCollectionなどへバインドするのではなく、XAML上でCollectionViewSourceをResourceとして定義して、それを経由してバインドすることができます。
こうしておくと、CollectionViewSourceのソート条件やフィルタ条件を変更することで、表示する対象やその並び順を変更することができます。
このフィルタ条件の変更を、LivetのInteractionMessageActionとして作成した例です。

まず、Action本体。
フィルタ条件変更後の表示件数をMessageのResponseとして返します。

using System.Linq;
using System.Windows;
using System.Windows.Data;
using Livet.Behaviors.Messaging;
using Livet.EventListeners.WeakEvents;
using Livet.Messaging;

namespace Bells.Behaviors
{
    /// <summary>
    /// CollectionViewSourceのFilterの内容を変更し、同時にその表示内容に更新するAction 
    /// </summary>
    public class SetCollectionViewSourceFilterAction : InteractionMessageAction<Window>
    {
        private LivetWeakEventListener<FilterEventHandler, FilterEventArgs> _listener; 

        protected override void InvokeAction(InteractionMessage m)
        {
            var msg = m as FilterMessage;
            if (msg == null) return;

            var cvs = (CollectionViewSource)(AssociatedObject.FindResource(msg.CollectionViewSourceKey));
                
            _listener = new LivetWeakEventListener<FilterEventHandler, FilterEventArgs>
                (
                h => new FilterEventHandler(h),
                h => cvs.Filter += h,
                h => cvs.Filter -= h,
                (sender,e)=> msg.FilterEventHandler(sender,e)
                );

            cvs.IsLiveFilteringRequested = true;
            cvs.IsLiveFilteringRequested = false;
            msg.Response = cvs.View.Cast<object>().Count();
        }
    }
}

上のActionに渡すMessageがこちら。

using System.Windows;
using System.Windows.Data;
using Livet.Messaging;

namespace Bells.Behaviors
{
    /// <summary>
    /// Filter変更用のメッセージ。Responseはint型でフィルター適用後の件数を受け取る。
    /// </summary>
    public class FilterMessage : ResponsiveInteractionMessage<int>
    {
        public FilterMessage()
        {
        }

        public FilterMessage(string messageKey): base(messageKey)
        {
        }

        public FilterMessage(string collectionViewSourceKey, FilterEventHandler filterEventHandler, string messageKey)
            : this(messageKey)
        {
            CollectionViewSourceKey = collectionViewSourceKey;
            FilterEventHandler = filterEventHandler;
            _msgKey = messageKey;
        }

        private readonly string _msgKey;
        public string CollectionViewSourceKey { get; set; }
        public FilterEventHandler FilterEventHandler { get; set; }

        protected override Freezable CreateInstanceCore()
        {
            return new FilterMessage(CollectionViewSourceKey, FilterEventHandler, _msgKey);
        }
    }
}

これを利用するViewのXAMLでは、まず、

<Window.Resources>
  <CollectionViewSource Source="{Binding BellCollection}" x:Key="BellSource1"/>
</Window.Resources>

などとResourceの中にCollectionViewSourceを定義し、そちらからViewModelの本来のバインドすべき大元のCollectionなどへバインドしておき、さらに、

<ListBox ItemsSource="{Binding Source={StaticResource BellSource1}}"/>

と、表示するListBoxなどからはこのCollectionViewSourceをItemsSourceに指定しておきます。

また、ViewModelからMessageが投げられたときに肝心のActionを動作させるために

<i:Interaction.Triggers>
  <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="SetFilter">
    <behaviors:SetCollectionViewSourceFilterAction/>
  </l:InteractionMessageTrigger>
</i:Interaction.Triggers>

の記述も必要です。View側は以上。

続いて、実際にこのActionをViewModelから呼ぶためには、まずFilterイベントハンドラを作っておく必要があります。

private void Filter(object sender, FilterEventArgs e)
{
	//e.Item にobject型でCollectionViewsSourceの各行のレコードが入ってくるので(レコード全件と同じ回数呼ばれます)
	//元の型にキャストして、そのプロパティなどを元に判断し、表示するかしないか決定します。
	//e.Accepted をtrue/falseに設定するかでその行の表示/非表示を決定してreturn。
}

で、実際に、何かボタンを押した時にフィルタ条件を変更する場合には、

public void Search()
{
    var msg = new FilterMessage("BellSource1", Filter, "SetFilter");
    Messenger.Raise(msg);
    if (msg.Response == 0)
        Messenger.Raise(new InformationMessage("該当レコードなし", "該当レコードなし", MessageBoxImage.Information, "Info"));
}

こんな感じになりますね。フィルタ条件変更後、1件以上何か表示されるならそれで終了。条件に合うレコードが0件で何も表示されない時だけメッセージボックスも表示しています。

WPFのListBoxで、自動で連番を割り振るConverter

ListBoxへの表示などで、単に画面上でだけ連番を振りたいことがあります。そんな時に簡単に使えるコンバーターです。
とりあえず、ItemsSourceから、ObservableCollectionやLivetのDispatcherCollectionなどへ直接バインドしている場合と、フィルターやソート処理を行うためにXAML上でCollectionViewSourceを定義してそれを挟んでバインドしている場合に対応しています。今のところそれ以外をItemsSourceとする需要がないのでそれ以外は未対応です。

まず、Converterの定義。

using System;
using System.Collections;
using System.Globalization;
using System.Windows.Data;

namespace Bells.Converters
{
    /// <summary>
    /// 連番を自動で振るコンバーター。
    /// 第一引数に連番を振る対象の要素、第二引数にそれを格納するコレクションを渡す
    /// </summary>
    class AutoEnumerator : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            var v = values[0];

            //直接コレクションなどにバインドしている場合
            var list = values[1] as IList;
            if (list!=null) return (list.IndexOf(v) + 1).ToString();

            //CollectionViewSourceをはさんでいる場合
            var view = values[1] as CollectionView;
            if(view!=null) return (view.IndexOf(v)+1).ToString();

            //それ以外は未対応
            throw new NotImplementedException();
        }
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


次にこれを利用するXAML側。StaticResourceとして定義する部分は省略します。ListBox1の中からDataTemplateで定義されたTextBlockに連番を表示するものとして、

<TextBlock>
  <TextBlock.Text>
    <MultiBinding Converter="{StaticResource AutoEnumerator}" Mode="OneTime">
      <Binding/>
      <Binding Path="ItemsSource" ElementName="ListBox1"></Binding>
    </MultiBinding>
  </TextBlock.Text>
</TextBlock>

こんな感じになります。やってることは単純で、ItemsSourceの中の1アイテムとItemsSourceそのものをConverterに渡して、その1アイテムのIndexを調べて、連番は1から始めるために+1して、さらにTextとして表示するためToString()してるだけです。

とりあえず第1回の小ネタとしてはこんなところで。