< C# には friend キーワードがない | メモ: ソート可能な ListView >

November 17, 2008

A Sortable GridView (I mean ListView) Control in WPF(翻訳中)

A Sortable GridView (I mean ListView) Control in WPF

I’ve been tinkering with WPF over the last couple of weeks or so and now that I’m getting a bit comfortable in it, I decided to make a ListView I was working with (that contains a GridView) sortable based upon the column header being clicked. After doing some research, I came across the MSDN how-to article that described a basic way of accomplishing this task. This blog post also has the same example explained. Unfortunately, though, it relied upon using the Column Header text as the property name on the underlying colllection on which to sort. After implementing the example, I decided this stank. Actually, I realized this stank beforehand, but wanted to implement the example anyways.

この二週間ほど WPF の調整を行ってきて、少しばかり満足できるものを得た今、クリックされたカラムヘッダに基づいてソートすることが出来る(GridView を含む)リストビューを作ることに決めました。調べてみたところ、このタスクを達成する基本的な方法を公開した MSDN の how-to 記事を見つけました。このブログ記事もまた、同じ例を説明しています。しかし残念ながらそれは、根底にあるコレクション上でのプロパティ名としてどれをソートに使用するかを、カラムヘッダのテキストによって得るものでした。この例を実行した後、これはいけてないことがはっきりしました。。 実は、私は以前よりこれはいけてないとわかってましたが、いずれにせよ例を実行したかったのです。

It stinks because Column Headers can contain anything. They don’t necessarily contain text. They could contain a button, a list, a drawing, a 3D animation, etc. Furthermore, even if they did just contain plain text, who’s to say that the text in the column header is actually a property name on my underlying object? What if I used “Birth Date” as the column header text and the property value was “DOB?” While the example was simple and made to be that way, I wanted a bit more control of how my GridView sorted.

これがいけてない理由は、カラムヘッダが何でも含むことができることです。それらは必ずしもテキストを含むというわけではありません。ボタン、リスト、ドローイング、3Dアニメーションなどを含むことが出来ます。しかも、プレーンテキストのみが含まれていたとしても、それが下層のオブジェクトの有効なプロパティ名だと誰が言えるでしょうか。仮に私が、カラムヘッダのテキストとして "Birth Date" を使用し、プロパティ値は "DOB?" だったとすれば?(訳注: 「and the property name was "DOB" ? 」の誤りか?)

I’m not going to dive into all the problems I see with the GridView and the ViewBase aspects of WPF in this blog, but that isn’t to say that I don’t think there are a ton of problems with this half-assed implementation. I’m just going to talk about the simple solution that I came up with.

私は WPF の GridView と ViewBase 層によると考えられるこれらすべての問題にこのブログでつっこんで取り組むつもりはありません。しかし、とてつもなく多くの問題がこの中途半端な実装のせいだとは思わないと言うべきでもありません。私はただ、自分がたどり着いたごくシンプルな解決策について述べようと思います。

I decided that in order to make my solution semi-intuitive, I needed to describe on the column level, what property on the underlying object it should sort on.

私は解決策を作成するにあたり、ソートすべき下層のオブジェクトのプロパティが何かを列レベルで記述する必要があるに違いないと半ば直感的に思いました。

Something that looks like this written in XAML:

XAML で書くとしたらたとえばこんな感じ:

<GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" SortPropertyName="ID" Width="50" />

That being said, if I wanted to add properties I needed to extend the current GridViewColumn object to contain them so I was really talking about creating a new Grid View Column, extending the existing one.

そうは言っても、プロパティを追加したいとすれば、それらを含有するために現在の GridViewColumn オブジェクトを拡張する必要があります。だからほんとに今あるやつを拡張して新しい Grid View Column をつくることについて述べます。

So my XAML would really look like:

だから実際には、私の XAML はこんな感じになります:

<local:SortableGridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" SortPropertyName="ID" IsDefaultSortColumn="True" Width="50" />

where local is defined as the namespace that points back to the namespace in which my new control is defined. Something like:

local は前の方の、私の新しいコントロールを定義した場所で、名前空間として定義されています。こんな風に:

xmlns:local="clr-namespace:ModelingTool"

That being said I needed to define my new SortableGridViewColumn. This was actually quite easy. I created a new CS file named SortableGridViewColumn.cs and then inherited from GridViewColumn and added a new dependency property called SortPropertyName

また、新しい SortableGridViewColumn を定義する必要があります。これは実のところものすごく簡単でした。SortableGridViewColumn.cs という名前で新規 CS ファイルを作成して、GridViewColumn を継承し、SortPropertyName という依存関係プロパティを追加しました。

Here’s the code.

それがこのコードです。

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
 
namespace ThreatModel
{
    public class SortableGridViewColumn : GridViewColumn
    {
 
        public string SortPropertyName
        {
            get { return (string)GetValue(SortPropertyNameProperty); }
            set { SetValue(SortPropertyNameProperty, value); }
        }
 
        // Using a DependencyProperty as the backing store for SortPropertyName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SortPropertyNameProperty =
            DependencyProperty.Register("SortPropertyName", typeof(string), typeof(SortableGridViewColumn), new UIPropertyMetadata(""));
 
 
        public bool IsDefaultSortColumn
        {
            get { return (bool)GetValue(IsDefaultSortColumnProperty); }
            set { SetValue(IsDefaultSortColumnProperty, value); }
        }
 
        public static readonly DependencyProperty IsDefaultSortColumnProperty =
            DependencyProperty.Register("IsDefaultSortColumn", typeof(bool), typeof(SortableGridViewColumn), new UIPropertyMetadata(false));
 
    }
}

As you can see, I'm not doing anything real fancy here, just adding two dependency properties. One that provides the string name of the property that column should sort on when applied to the object. And one that sets a Boolean property to indicate if this column is the default sort column.

見ての通り、私は本当に何もしていません。ただ二つの依存関係プロパティを追加したのみです。ひとつはオブジェクト適用下での、カラムをソートすべきオブジェクト上のプロパティ名を与えるものです。そしてもうひとつはそのカラムがデフォルトでソートするカラムかどうかを示す真偽値を設定するものです。

Now, whenever I declare my GridView’s columns, I use the

現在、いつも GridView の列はこんな

<local:SortableGridViewColumn Header="Element Name" DisplayMemberBinding="{Binding ElementName}" SortPropertyName="ElementName" />

syntax.

構文で定義します。

Defining the new SortableGridViewColumn control was the first step. For true encapsulation, the ListView should actually handle all of the sorting internally. (In fact, it should be the GridView that handles the sorting internally. Unfortunately, the GridView doesn’t provide any mechanism to accomplish this as it isn’t a real control, but is actually a View; a very important difference.)

新しい SortableGridViewColumn コントロールを定義するのは第一段階でした。真のカプセル化には、実際には ListView は内部的にソートのすべてを処理しなければなりません。(本当のところ、内部的にソートを扱うのは GridView であるべきです。残念なことに、GridView はコントロールではなく、実際のところビューであるため、それを実現するための何のメカニズムも提供しません。それは重要な違いです。)

To complete the ListView’s sorting capabilities, we have to create a new ListView that encapsulates the sorting functionality. We’ll call it the SortableListView and it’ll inherit from ListView.

ListView のソート機能を完成させるには、ソート機能をカプセル化した新しい ListView を作らないといけません。SortableListView と呼び、ListView より継承するようにしましょう。

Here's the code in all it's glory.

これがその栄えあるコード全体です。

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Collections;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Media;
 
namespace SortableWPFGridView
{
 
    // if the GridView exposed any methods at all that allowed for overriding at a control level, I would be
    // able to do all of this work inside it rather than the ListView. However, b/c it doesn't, I have to do the
    // work inside the ListView.
 
    // The GridView has access to the ItemSource on the ListView through the dependency property mechanism.
 
    public class SortableListView : ListView
    {
        SortableGridViewColumn lastSortedOnColumn = null;
        ListSortDirection lastDirection = ListSortDirection.Ascending;
 
 
        #region New Dependency Properties
 
        public string ColumnHeaderSortedAscendingTemplate
        {
            get { return (string)GetValue(ColumnHeaderSortedAscendingTemplateProperty); }
            set { SetValue(ColumnHeaderSortedAscendingTemplateProperty, value); }
        }
 
        // Using a DependencyProperty as the backing store for ColumnHeaderSortedAscendingTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ColumnHeaderSortedAscendingTemplateProperty =
            DependencyProperty.Register("ColumnHeaderSortedAscendingTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));
 
 
        public string ColumnHeaderSortedDescendingTemplate
        {
            get { return (string)GetValue(ColumnHeaderSortedDescendingTemplateProperty); }
            set { SetValue(ColumnHeaderSortedDescendingTemplateProperty, value); }
        }
 
        // Using a DependencyProperty as the backing store for ColumnHeaderSortedDescendingTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ColumnHeaderSortedDescendingTemplateProperty =
            DependencyProperty.Register("ColumnHeaderSortedDescendingTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));
 
 
        public string ColumnHeaderNotSortedTemplate
        {
            get { return (string)GetValue(ColumnHeaderNotSortedTemplateProperty); }
            set { SetValue(ColumnHeaderNotSortedTemplateProperty, value); }
        }
 
        // Using a DependencyProperty as the backing store for ColumnHeaderNotSortedTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ColumnHeaderNotSortedTemplateProperty =
            DependencyProperty.Register("ColumnHeaderNotSortedTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));
 
 
        #endregion
 
        ///
        /// Executes when the control is initialized completely the first time through. Runs only once.
        ///
        ///
        protected override void OnInitialized(EventArgs e)
        {
            // add the event handler to the GridViewColumnHeader. This strongly ties this ListView to a GridView.
            this.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(GridViewColumnHeaderClickedHandler));
 
            // cast the ListView's View to a GridView
            GridView gridView = this.View as GridView;
            if (gridView != null)
            {
                // determine which column is marked as IsDefaultSortColumn. Stops on the first column marked this way.
                SortableGridViewColumn sortableGridViewColumn = null;
                foreach (GridViewColumn gridViewColumn in gridView.Columns)
                {
                    sortableGridViewColumn = gridViewColumn as SortableGridViewColumn;
                    if (sortableGridViewColumn != null)
                    {
                        if (sortableGridViewColumn.IsDefaultSortColumn)
                        {
                            break;
                        }
                        sortableGridViewColumn = null;
                    }
                }
 
                // if the default sort column is defined, sort the data and then update the templates as necessary.
                if (sortableGridViewColumn != null)
                {
                    lastSortedOnColumn = sortableGridViewColumn;
                    Sort(sortableGridViewColumn.SortPropertyName, ListSortDirection.Ascending);
 
                    if (!String.IsNullOrEmpty(this.ColumnHeaderSortedAscendingTemplate))
                    {
                        sortableGridViewColumn.HeaderTemplate = this.TryFindResource(ColumnHeaderSortedAscendingTemplate) as DataTemplate;
                    }
 
                    this.SelectedIndex = 0;
                }
            }
 
            base.OnInitialized(e);
        }
 
        ///
        /// Event Handler for the ColumnHeader Click Event.
        ///
        ///
        ///
        private void GridViewColumnHeaderClickedHandler(object sender, RoutedEventArgs e)
        {
            GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
 
            // ensure that we clicked on the column header and not the padding that's added to fill the space.
            if (headerClicked != null && headerClicked.Role != GridViewColumnHeaderRole.Padding)
            {
                // attempt to cast to the sortableGridViewColumn object.
                SortableGridViewColumn sortableGridViewColumn = (headerClicked.Column) as SortableGridViewColumn;
 
                // ensure that the column header is the correct type and a sort property has been set.
                if (sortableGridViewColumn != null && !String.IsNullOrEmpty(sortableGridViewColumn.SortPropertyName))
                {
 
                    ListSortDirection direction;
                    bool newSortColumn = false;
 
                    // determine if this is a new sort, or a switch in sort direction.
                    if (lastSortedOnColumn == null
                        || String.IsNullOrEmpty(lastSortedOnColumn.SortPropertyName)
                        || !String.Equals(sortableGridViewColumn.SortPropertyName, lastSortedOnColumn.SortPropertyName, StringComparison.InvariantCultureIgnoreCase))
                    {
                        newSortColumn = true;
                        direction = ListSortDirection.Ascending;
                    }
                    else
                    {
                        if (lastDirection == ListSortDirection.Ascending)
                        {
                            direction = ListSortDirection.Descending;
                        }
                        else
                        {
                            direction = ListSortDirection.Ascending;
                        }
                    }
 
                    // get the sort property name from the column's information.
                    string sortPropertyName = sortableGridViewColumn.SortPropertyName;
 
                    // Sort the data.
                    Sort(sortPropertyName, direction);
 
                    if (direction == ListSortDirection.Ascending)
                    {
                        if (!String.IsNullOrEmpty(this.ColumnHeaderSortedAscendingTemplate))
                        {
                            sortableGridViewColumn.HeaderTemplate = this.TryFindResource(ColumnHeaderSortedAscendingTemplate) as DataTemplate;
                        }
                        else
                        {
                            sortableGridViewColumn.HeaderTemplate = null;
                        }
                    }
                    else
                    {
                        if (!String.IsNullOrEmpty(this.ColumnHeaderSortedDescendingTemplate))
                        {
                            sortableGridViewColumn.HeaderTemplate = this.TryFindResource(ColumnHeaderSortedDescendingTemplate) as DataTemplate;
                        }
                        else
                        {
                            sortableGridViewColumn.HeaderTemplate = null;
                        }
                    }
 
                    // Remove arrow from previously sorted header
                    if (newSortColumn && lastSortedOnColumn != null)
                    {
                        if (!String.IsNullOrEmpty(this.ColumnHeaderNotSortedTemplate))
                        {
                            lastSortedOnColumn.HeaderTemplate = this.TryFindResource(ColumnHeaderNotSortedTemplate) as DataTemplate;
                        }
                        else
                        {
                            lastSortedOnColumn.HeaderTemplate = null;
                        }
                    }
                    lastSortedOnColumn = sortableGridViewColumn;
                }
            }
        }
 
        ///
        /// Helper method that sorts the data.
        ///
        ///
        ///
        private void Sort(string sortBy, ListSortDirection direction)
        {
            lastDirection = direction;
            ICollectionView dataView = CollectionViewSource.GetDefaultView(this.ItemsSource);
 
            dataView.SortDescriptions.Clear();
            SortDescription sd = new SortDescription(sortBy, direction);
            dataView.SortDescriptions.Add(sd);
            dataView.Refresh();
        }
    }
}
 

It might seem like a lot, but it really isn’t.

ものすごく多く見えるかもしれませんが、実際にはそうではありません。

The dependency properties at the top,

先頭の依存関係プロパティは、(以下の部分)

#region New Dependency Properties

public string ColumnHeaderSortedAscendingTemplate
{
    get { return (string)GetValue(ColumnHeaderSortedAscendingTemplateProperty); }
    set { SetValue(ColumnHeaderSortedAscendingTemplateProperty, value); }
}

// Using a DependencyProperty as the backing store for ColumnHeaderSortedAscendingTemplate.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnHeaderSortedAscendingTemplateProperty =
    DependencyProperty.Register("ColumnHeaderSortedAscendingTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));


public string ColumnHeaderSortedDescendingTemplate
{
    get { return (string)GetValue(ColumnHeaderSortedDescendingTemplateProperty); }
    set { SetValue(ColumnHeaderSortedDescendingTemplateProperty, value); }
}

// Using a DependencyProperty as the backing store for ColumnHeaderSortedDescendingTemplate.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnHeaderSortedDescendingTemplateProperty =
    DependencyProperty.Register("ColumnHeaderSortedDescendingTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));


public string ColumnHeaderNotSortedTemplate
{
    get { return (string)GetValue(ColumnHeaderNotSortedTemplateProperty); }
    set { SetValue(ColumnHeaderNotSortedTemplateProperty, value); }
}

// Using a DependencyProperty as the backing store for ColumnHeaderNotSortedTemplate.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnHeaderNotSortedTemplateProperty =
    DependencyProperty.Register("ColumnHeaderNotSortedTemplate", typeof(string), typeof(SortableListView), new UIPropertyMetadata(""));

#endregion

provide a mechanism to dynamically style the column header’s based upon if that column is sorted ascending, descending, or not sorted at all.

カラムヘッダの、カラムが昇順でソートされているか、降順か、またはまったくソートされていないかを元にして、動的に表示するメカニズムを提供します。

Once you get past that, the rest of the code is similar to the code listed in the above MSDN example.

それを乗り越えてしまえば、コードの残りの部分は前の MSDN の例と酷似しています。

A major differences is the inclusion of the overridden OnInitialized method.

この中の目立った違いとしては、 OnInitialized メソッドのオーバーライドでしょうか

///
/// Executes when the control is initialized completely the first time through. Runs only once.
///
///
protected override void OnInitialized(EventArgs e)
{
    // add the event handler to the GridViewColumnHeader. This strongly ties this ListView to a GridView.
    this.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(GridViewColumnHeaderClickedHandler));

    // cast the ListView's View to a GridView
    GridView gridView = this.View as GridView;
    if (gridView != null)
    {
        // determine which column is marked as IsDefaultSortColumn. Stops on the first column marked this way.
        SortableGridViewColumn sortableGridViewColumn = null;
        foreach (GridViewColumn gridViewColumn in gridView.Columns)
        {
            sortableGridViewColumn = gridViewColumn as SortableGridViewColumn;
            if (sortableGridViewColumn != null)
            {
                if (sortableGridViewColumn.IsDefaultSortColumn)
                {
                    break;
                }
                sortableGridViewColumn = null;
            }
        }

        // if the default sort column is defined, sort the data and then update the templates as necessary.
        if (sortableGridViewColumn != null)
        {
            lastSortedOnColumn = sortableGridViewColumn;
            Sort(sortableGridViewColumn.SortPropertyName, ListSortDirection.Ascending);

            if (!String.IsNullOrEmpty(this.ColumnHeaderSortedAscendingTemplate))
            {
                sortableGridViewColumn.HeaderTemplate = this.TryFindResource(ColumnHeaderSortedAscendingTemplate) as DataTemplate;
            }

            this.SelectedIndex = 0;
        }
    }

    base.OnInitialized(e);
}

In this method I wire up the GridView’s Click Event to a new RoutedEventHandler, GridViewColumnHeaderClickedHandler.

このメソッドで、GridView のクリックイベントに新しい RoutedEventHandler、GridViewColumnHeaderClickedHandler を仕込んでいます。

// add the event handler to the GridViewColumnHeader. This strongly ties this ListView to a GridView.
this.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(GridViewColumnHeaderClickedHandler));

This isn’t the best programming I’ve ever done, but it does the trick. At this point, I’ve tightly coupled the GridView and ListView together. Echh! Since the ListView might contain another View other than the GridView (although 3.0 only contains the GridView), this is fairly brittle code and well, should eventually be tossed in the garbage. But for now, it works because of the limitations of the GridView View.

これは私の今までで最高のプログラミングではありませんが、目的は果たせます。GridView と ListView を互いにきつく結び付けた点で。イー!ListView が GridView 以外のビューも含むかもしれないですし (3.0 では GridView のみとはいえ), これは相当脆いコードで、ゴミ箱の中へ放り込まれる運命にあります。しかし現在のところ、GridView ビューに限られるためにこれで動作します。

トラックバック

このエントリーにトラックバック:
http://frog.raindrop.jp/cgi-bin/mt/mt-tb.cgi/2243

コメント

こんにちは~。いきなりのカキコ失礼します。
みんなちゃんと筋トレ頑張ってますね!小生は外回りが多いので仕事が忙しいし食べるものも適当筋トレも適当でサプリとか2日ぐらい開けちゃったり。やっぱり口にしなくなっちゃったりダメダメです。
だから最近ではやっぱりサプリに頼ってますよ。もちろん錠剤のサプリ。錠剤のサプリなら持ち運べるしペットボトルさえあればそこそこ手早く。一服する時に軽くスクワットとかやってロッカールームでサプリ飲み込んでますよ。
あ、ちなみにHMBならたんぱく質摂るよりもよりも効きが良いみたい。アミノ酸のロイシンの代謝物質だって知ってました?アミノ酸で知られるロイシンもHMBに変化しないと動けないんだそうですよ。更にHMBはロイシン20gから1gしかできてこないそうだからHMBを摂ることは効率のよいことになる。我々みたいな昼間働く人間には効きがいいのが一番助かるって感じですよ

コメントする

※ コメントスパム対策のため、コメント本文はおはよう、こんにちわ、こんばんわのいずれかより始めるようにしてください。

name:
email:

※ 必要ですが、表示しません。

url:
情報を保存する ?