RunningCSharp

MS系開発者による、雑多な記事。記事は所属企業とは関係のない、個人の見解です。

WPF:DataGridの列ヘッダークリックによるソートを「昇順」→「降順」→「ソート無し」の繰り返しにカスタム

WPFのデータグリッドの標準的な列ヘッダークリックソートの動きは「昇順」→「降順」→「昇順」…の繰り返しなのですが、 これを「昇順」→「降順」→「ソート無し(データソースの並び順)」→「昇順」…の繰り返しに変更します。

DataGrid向けの添付プロパティを用意します。

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace DataGridSort
{
    public class Attached
    {
        public static bool GetIsSortCustomize(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsSortCustomizeProperty);
        }

        public static void SetIsSortCustomize(DependencyObject obj, bool value)
        {
            obj.SetValue(IsSortCustomizeProperty, value);
        }

        //Trueに設定した場合、ソートを昇順、降順、なしの順で行われるカスタムが施される
        public static readonly DependencyProperty IsSortCustomizeProperty =
            DependencyProperty.RegisterAttached("IsSortCustomize", typeof(bool), typeof(Attached), new PropertyMetadata(OnIsSortCustomizeChanged));

        private static void OnIsSortCustomizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var datagrid = d as DataGrid;
            if (datagrid == null) return;
            if ((bool)e.NewValue)
            {
                datagrid.Sorting += Datagrid_Sorting;
            }
            else
            {
                datagrid.Sorting -= Datagrid_Sorting;
            }
        }

        private static void Datagrid_Sorting(object sender, DataGridSortingEventArgs e)
        {
            var datagrid = sender as DataGrid;
            if (datagrid.ItemsSource == null) return;

            var listColView = (ListCollectionView)CollectionViewSource.GetDefaultView(datagrid.ItemsSource);
            if (listColView == null) return;

            if (e.Column.SortDirection == ListSortDirection.Descending)
            {
                //ソートを中断
                e.Handled = true;
                //ソートの方向をクリア
                e.Column.SortDirection = null;
                datagrid.Items.SortDescriptions.Clear();
            }
        }
    }
}

上記の添付プロパティを、下記のような形で使用します。

<Window
        xmlns:atpr="clr-namespace:DataGridSort"

><Grid>
        <DataGrid … atpr:Attached.IsSortCustomize="True"/>
    </Grid>
</Window>

上記の実装を行い実行し、

f:id:ys-soniclab:20160827113454p:plain

列ヘッダーをクリックすると、

f:id:ys-soniclab:20160827113544p:plain

昇順ソート、

f:id:ys-soniclab:20160827113631p:plain

降順ソート、

f:id:ys-soniclab:20160827113648p:plain

ソート無し、の順に遷移し、もう一度クリックすると昇順ソートを行います。

Xamarin:PCLStorageを用いたファイル操作

Xamarinでローカルストレージのファイル操作が出来るライブラリ「PCL Storage」を扱ってみたのでメモしておきます。

まずNugetでpclstorageなどの文言で検索し、「PCL Storage」をダウンロードして使います。

f:id:ys-soniclab:20160820012650p:plain

下記のようなコードでファイル操作が出来ました。

using PCLStorage;
…

//ローカルストレージの位置を取得
var localstrage = FileSystem.Current.LocalStorage;
//フォルダチェック
var folderresult = await localstrage.CheckExistsAsync("DataFolder");
if (folderresult != PCLStorage.ExistenceCheckResult.FolderExists)
{
    //フォルダが見つからない場合の処理をここに記載
}
//フォルダが無ければ作成して移動、フォルダがあれば移動
var filefolder = await localstrage.CreateFolderAsync("DataFolder", CreationCollisionOption.OpenIfExists);
//ファイルチェック
var fileresult = await filefolder.CheckExistsAsync("Data.txt");
if (fileresult != PCLStorage.ExistenceCheckResult.FileExists)
{
    //ファイルが無ければ作成
    var savefile = await filefolder.CreateFileAsync("Data.txt", CreationCollisionOption.ReplaceExisting);
    await savefile.WriteAllTextAsync("filetext…filetext");
}
//ファイル読み込み
var loadfile = await filefolder.GetFileAsync("Data.txt");
var loadText = await loadfile.ReadAllTextAsync();

Xamarin:Xamarinで添付プロパティ(Attached Property)を試す

WPFではViewのコントロールに独自の値を渡したり、独自の値の変更を契機にイベントを動かしたい時などに添付プロパティを使っていました。 Xamarinでも使えるようなので、とりあえずシンプルな実装を試してみました。

添付プロパティの実装は以下の通り。

public class Attached
{
    public static readonly BindableProperty TestProperty =
    BindableProperty.CreateAttached("Test", typeof(bool), typeof(Attached), false, propertyChanging: OnTestChanged);

    private static void OnTestChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var editor = bindable as Editor;
        if ((bool)newValue)
        {
            //フォーカスが離れた際にイベント実行
            editor.Unfocused += Editor_Unfocused; ;
        }
        else
        {
            editor.Unfocused -= Editor_Unfocused;
        }
    }

    private static void Editor_Unfocused(object sender, FocusEventArgs e)
    {
        var editor = sender as Editor;
        //テキストを赤に。
        editor.TextColor = Color.Red;
    }
}

添付プロパティの適用個所は以下のような感じで。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AttachedApp.MainPage"
             xmlns:local="clr-namespace:AttachedApp">
  <Editor local:Attached.Test="True" Text="TestEditor" VerticalOptions="Center" HorizontalOptions="Center" />
</ContentPage>

UWPでの動作イメージはこんな感じです。

・起動時

f:id:ys-soniclab:20160815234422p:plain

・テキストボックスからフォーカスが外れた時

f:id:ys-soniclab:20160815234436p:plain

この例ですと、Editorにフォーカスが移ると文字が青くなるだけの簡素な内容ですが、 ViewModelからコマンドを渡してあげたりイベントで使いたい値を渡してあげたり、幅広い用途が考えられそうです。

Xamarin:Gridアイテムの間を詰める

例えばこんな感じのxamlを書いた場合、

 <Grid>
    <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Button Text="test" VerticalOptions="Fill" HorizontalOptions="Fill" />
    <Button Grid.Row="1" Text="test" VerticalOptions="Fill" HorizontalOptions="Fill" />
    <Button Grid.Column="1" Text="test" VerticalOptions="Fill" HorizontalOptions="Fill" />
    <Button Grid.Column="1" Grid.Row="1"  Text="test" VerticalOptions="Fill" HorizontalOptions="Fill" />
  </Grid>

f:id:ys-soniclab:20160811152839p:plain

こんな感じでグリッド内アイテムの間に隙間が出来ます。

GridのRowSpacingプロパティとColumnSpacingプロパティのデフォルト値が0でないことが原因のようです。

そこで

<Grid RowSpacing="0">

と設定すると、

f:id:ys-soniclab:20160811152949p:plain

行間の隙間が埋まります。

更に

<Grid RowSpacing="0" ColumnSpacing="0">

と設定すると、

f:id:ys-soniclab:20160811153119p:plain

列の隙間も埋まります。

GitHub:はてなブログ上にGitHubにアップロードしたソースコードの表示を試してみる

https://gist-it.appspot.com/

上記リンクを参考にしながら、この間アップロードしたソースコードの表示を試してみます。

github.com

上記URLにアップされたソースコードの中で、「RadioButtonList.cs」を表示する場合は、

<script src="http://gist-it.appspot.com/github/ysrun/sample_radiobuttonlist/blob/master/radiobuttonlist/radiobuttonlist/RadioButtonList.cs" ></script>

上記のタグをブログ本文中に入れることで、ファイル全文が表示できました。 (全文で表示すると結構量があったため、ここでは省略いたします。)

恐らくブログで一番使うのは、行番号を指定しての表示かなと考えております。

f:id:ys-soniclab:20160806224812p:plain

上記GitHubのサイト上表示によると「14行目~31行目」となっているOnApplyTemplateメソッドを表示してみようと思います。 なお、参考サイトによると行数は0始まりのようなので、13行目から30行目の指定をしたいと思います。

「?slice=13:30"」という表記をスクリプトタグの属性値に加えてみます。

<script src="http://gist-it.appspot.com/github/ysrun/sample_radiobuttonlist/blob/master/radiobuttonlist/radiobuttonlist/RadioButtonList.cs?slice=13:30" ></script>

パッと見たところ、最後2行が切れているように見えます、フッターの表示部分と被ってしまっているのかもしれません。 表示行を2行増やしてみますと、

<script src="http://gist-it.appspot.com/github/ysrun/sample_radiobuttonlist/blob/master/radiobuttonlist/radiobuttonlist/RadioButtonList.cs?slice=13:32" ></script>

こんな感じで表示されます。

C#:補完文字列に関するTips

C#4.6以降で使える補完文字列でも、改行や\をそのまま扱いたいときは@が使えます。

string test = $@"C:\test
D:\test";

また、補完対象の中括弧内に関数を指定する事も可能でした。

string test1 = "test1";
string interpolation = "補完したい文字列:{func(test1)}";
… 
static string func(string teststr) => teststr + "add";

更に中括弧内で使う関数の引数に文字列リテラルを使用することも可能で、更にその文字列リテラルに$を付けて補完文字列を扱えるなど、非常に柔軟な仕様のようです。

string test1 = "and";
string interpolation = $"{func(@"\te" + $"st{test1}")}";
Console.WriteLine(interpolation);
… 
static string func(string teststr) => teststr + "add";

結果

\testandadd

WPF:配列からRadioButtonを自動生成するItemsControlを作成

タイトルの通りItemsSourceにboolとstringを持ったオブジェクトの配列を渡すことでRadioButtonを生成するコントロールのサンプルです。

サンプルはGitHubにアップしてみました。 github.com

ItemsControlを継承したカスタムコントロールとなっております、利用時にはResourceDictionaryの読み込みが必要な感じになっています。ResourceDictionaryたいしたことは書いていませんが、改善で拡張する際にあったほうが便利かと思いつけました。

一応、使い方を。 ①RadioButtonList.csとRadioButtonList.xamlをプロジェクトに追加

②利用するView(ウィンドウなど)またはApp.xamlなど参照可能な場所に

<ResourceDictionary Source="/radiobuttonlist;component/RadioButtonList.xaml"/>

上記のリソースを読み込んだうえで、

<ctl:RadioButtonList xmlns:ctl="clr-namespace:radiobuttonlist"
ItemsSource="{Binding Items}" CheckedItem="{Binding CheckedItem}"
TitleMemberPath="Title" IsCheckedMemberPath="IsChecked"/>

上記のような感じのタグで配置します。

③ViewModel側ではItemsSourceに指定した配列クラスの「IsCheckプロパティにバインドしたいプロパティ名」を「IsCheckedMemberPath」に、 「キャプションとして表示したい文字列のプロパティ名」を「IsCheckedMemberPath」に指定します。

こんな感じで使えるかと。

例えば、上記のタグに下記のようなデータクラスをバインドした場合、

//ItemsはItemsSourceプロパティとバインドされたObservableCollection
Items.Add(new RadioButtonItem() { IsChecked = true, Title = "test1" });
Items.Add(new RadioButtonItem() { IsChecked = false, Title = "test2" });
Items.Add(new RadioButtonItem() { IsChecked = false, Title = "test3" });
Items.Add(new RadioButtonItem() { IsChecked = false, Title = "test4" });
Items.Add(new RadioButtonItem() { IsChecked = false, Title = "test5" });

下記のような画面が表示されます。

f:id:ys-soniclab:20160730230748p:plain

また、IsCheckedプロパティがTrueになったオブジェクトはCheckedItemプロパティに格納されます。


作成時、ItemTempleteの動的生成部分はちょっと作るのが大変でした。

//ItemTemplateをプロパティ値から動的に作成
var elementFactory = new FrameworkElementFactory(typeof(RadioButton));
if (!string.IsNullOrEmpty(TitleMemberPath))
{
    elementFactory.SetValue(RadioButton.ContentProperty, new Binding(TitleMemberPath));
}
if (!string.IsNullOrEmpty(IsCheckedMemberPath))
{
    elementFactory.SetValue(RadioButton.IsCheckedProperty, new Binding(IsCheckedMemberPath));
}
elementFactory.SetValue(RadioButton.GroupNameProperty, string.IsNullOrEmpty(GroupName) ? Guid.NewGuid().ToString() : GroupName);
elementFactory.AddHandler(RadioButton.CheckedEvent, new RoutedEventHandler(CheckedMethod));
this.ItemTemplate = new DataTemplate { VisualTree = elementFactory };

書きづらいし読みづらいとも思いますが、Bindingオブジェクトも色々手を付けられ、イベントに対応したメソッドも簡単に取り付けられるのは便利だと思いました。