20200519のC#に関する記事は8件です。

UniTask2(RC版)まとめ

UniTask2

UniTaskが大きくアップデートされて、バージョン2系がRCとなりました。
今回はいち早くその変更点をまとめていきます。

Ver 2.0.7-rc4準拠で書いています。変更があり次第また直します。

破壊的変更

まずは1系からの破壊的変更。導入時に気をつける必要があるところです。

名前空間の変更

名前空間がUniRx.AsyncからCysharp.Threading.Tasksに変更されました。
もともとUniRxのライブラリの一部だった名残なのですが、今回のアップデートで完全に名残がなくなりました。

UniTaskのawait2度漬け禁止

同じUniTaskに対しての2回以上のawaitが明確に禁止となりました。
IValueTaskSource準拠の挙動なので、これは本家ValueTaskと同じ挙動です。

UniTaskの2回以上のawait禁止
private async UniTaskVoid DoAsync(CancellationToken token)
{
    try
    {
        var uniTask = GetAsync("https://unity.com/ja", token);

        // 1回目のawaitは問題ない
        await uniTask;

        // 同じオブジェクトに対して2回以上のawaitはできない
        // (InvalidOperationExceptionが発行される)
        await uniTask;
    }
    catch (InvalidOperationException e)
    {
        Debug.LogException(e);
    }
}

private async UniTask<string> GetAsync(string uri, CancellationToken token)
{
    var uwr = UnityWebRequest.Get(uri);
    await uwr.SendWebRequest().ConfigureAwait(cancellation: token);
    return uwr.downloadHandler.text;
}

Preserve()

もし同じUniTaskを2回以上awaitする必要があるならば、Preserve()を利用しましょう。

Preserve()
private async UniTaskVoid DoAsync(CancellationToken token)
{
    try
    {
        var uniTask = GetAsync("https://unity.com/ja", token);

        // Preserve()で何回でもawait可能なUniTaskに変換
        var reusable = uniTask.Preserve();

        await reusable;
        await reusable;
    }
    catch (InvalidOperationException e)
    {
        Debug.LogException(e);
    }
}

UniTask.Lazyの戻り値の変更

上記の多重await禁止の変更をうけて、UniTask.Lazyの戻り値がAsyncLazyに変換されました。

public static AsyncLazy Lazy(Func<UniTask> factory)
public static AsyncLazy<T> Lazy<T>(Func<UniTask<T>> factory)

使い勝手としてはあまり変わってはいません。

UniTask.WhenAnyの戻り値変更

WhenAnyの戻り値も変更されました。

public static async UniTask<(int winArgumentIndex, (bool hasResult, T0 result0), (bool hasResult, T1 result1), (bool asResult, T2 result2))> 
WhenAny<T0, T1, T2>()
public static UniTask<(int winArgumentIndex, T1 result1, T2 result2, T3 result3)> WhenAny<T1, T2, T3>()

UniTask.DelayFrameの戻り値変更

UniTask.DelayFrameの戻り値がUniTask<int>からUniTaskに変更されました。

既存機能への追加機能とか

全体的なパフォーマンスの向上

全体的にパフォーマンスが向上しています。
内部でUniTaskAsyncMethodBuilderRunnerの再利用が自動的に行われるようになり、ゼロアロケーションで動作するようになりました。

UniTaskCompletionSourceが再利用可能に

UniTaskCompletionSourceReset()メソッドが追加されました。
こちらを利用することでUniTaskCompletionSourceを再利用することができるようになりました。

AutoResetUniTaskCompletionSource追加

AutoResetUniTaskCompletionSourceも追加されました。
こちらはUniTaskCompletionSourceと挙動は同じですが、破棄したときに内部のインスタンスが自動的に再利用される仕組みになっています。

UniTaskCompletionSourceとの使い分けですが、何度も同じインスタンスを再利用するならUniTaskCompletionSourceを使う。
使い捨てで都度新しいものを使うならAutoResetUniTaskCompletionSourceを使う。
という使い分けをするとよいでしょう。

ファクトリメソッドが追加

ファクトリメソッドがいくつか追加されました。

  • UniTask.Create
  • UniTask.Defer
  • UniTask.VoidAction
  • UniTask.VoidUnityAction
  • UniTask.WaitUntilCanceled

PlayerLoopTiming追加

PlayerLoopTimingが追加され、次の種類となりました。

  • Initialization
  • LastInitialization
  • EarlyUpdate
  • LastEarlyUpdate
  • FixedUpdate
  • LastFixedUpdate
  • PreUpdate
  • LastPreUpdate
  • Update
  • LastUpdate
  • PreLateUpdate
  • LastPreLateUpdate
  • PostLateUpdate
  • LastPostLateUpdate

とくにLastPostLateUpdateが重要で、こちらはコルーチンにおけるWaitForEndOfFrameに相当します。
いままでUniTaskではyield return new WaitForEndOfFrame()ができなかったのですが、今回のアップデートから可能となりました。

JobHandle.WaitAsync

JobSystemJobHandleWaitAsyncが追加されました。
こちらを利用することで、任意のPlayerLoopのタイミングに切り替えてから完了待ちができます。

// WaitForEndOfFrame相当の待機をしてからComplete()
await jobHandle.WaitAsync(PlayerLoopTiming.LastPostLateUpdate);

AsyncTriggerメソッドのIUniTaskAsyncEnumerable対応

AsyncTrigger系のメソッド(this.GetAsyncCollisionEnterTriggerとか)がIUniTaskAsyncEnumerableに対応しました。

新機能

UniTaskAsyncEnumerable/IUniTaskAsyncEnumerable

UniTaskAsyncEnumerableUniTask2の目玉機能です。

IUniTaskAsyncEnumerable<T>C# 8.0IAsyncEnumerable<T>UniTaskとして実装したものです。
なんとこちらはC# 7.x系のUnityでも利用可能になっています。

何をするためのものかというと、「非同期処理を複数個まとめて扱う」ことができるようになる機能です。
ObservableIObservable<T>)と似ていますが、こちらはPull型として機能するという違いがあります。

image.png

Observableとの使い分け

ObservablePush型なのに対して、UniTaskAsyncEnumerablePull型です。
そのためUniTaskAsyncEnumerableでは非同期処理の実行タイミングを受信側でコントロールできるというメリットがあります。

UniTaskAsyncEnumerableで複数の非同期処理を扱う
private async void Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    var uris = new[]
    {
        "https://www.google.com/",
        "https://unity.com/ja",
        "https://github.com/"
    };


    // URIのリストに対してアクセスしてデータをとってくる
    // ただし常に実行される通信は同時に1つであり、
    // 前回のものが完了しないと次の通信に進まない
    await uris.ToUniTaskAsyncEnumerable()
        // 通信完了を待機してメッセージを発行する
        .SelectAwait(async x => await FetchAsync(x))
        .ForEachAsync(x => Debug.Log(x), token);
}


private async UniTask<string> FetchAsync(string uri)
{
    using (var uwr = UnityWebRequest.Get(uri))
    {
        await uwr.SendWebRequest();
        if (uwr.isNetworkError || uwr.isHttpError)
        {
            throw new Exception($"Error>{uwr.error}");
        }

        return uwr.downloadHandler.text;
    }
}

対するObservablePush型のため、多数のObserverに対してメッセージをブロードキャストするのに向いています。

複数個の非同期処理を管理する場合は基本にはUniTaskAsyncEnumerableを使う。
イベント駆動を制御する場合にはObservableを使う。

という使い方をするとよいでしょう。

ForEachAsync/ForEachAwaitAsync

C# 8.0であればawait foreachが使えるのですが、それが使えないUnityバージョンでは代替としてForEachAsyncを利用します。
感覚としては、IObservable<T>に対するSubscribe()に似ています。
ですがこちらは「ForEachAsync()の完了をさらにawaitで待つ」といったことが可能となります。

ForEachAsync
private async void Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    // EveryUpdate()は毎フレームのタイミングで完了するUniTaskを返す
    await UniTaskAsyncEnumerable.EveryUpdate()
        .Select((_, x) => x)
        // 5回まで実行する
        .Take(5)
        // ForEachAsyncで待機する
        .ForEachAsync(async _ => Debug.Log(Time.frameCount), token);

    Debug.Log("Done!");
}

また、ForEachAsyncの他にForEachAwaitAsyncもあります。
こちらはデリゲートの内部でasync/awaitが利用でき、この非同期処理が完了するまで次のメッセージを取りに行きません。

ForEachAwaitAsync
await UniTaskAsyncEnumerable.EveryUpdate()
    .Select((_, x) => x)
    // 5回まで実行する
    .Take(5)
    // ForEachAwaitAsyncで待機する
    .ForEachAwaitAsync(async _ =>
    {
        // 10フレーム待ってから次のメッセージを取りに行く
        await UniTask.DelayFrame(10, cancellationToken: token);
    }, token);

注意:ForEachAsyncasync/awaitする

ミスしそうな点として、ForEachAsyncasync/awaitを書いてしまうことです。
こちらの場合、コンパイルエラーとならずに動作してしまうのにForEachAwaitAsyncと動作がまったく異なってしまいます。

UniTask2 RC4でこちらの挙動はコンパイルエラーとなるようになり、安全になりました。

もしForEachAsyncを使いつつ、UniTaskVoidなasync/awaitを使いたい(非同期処理だけどその結果待ちをせずに次のMoveNextAsync()を呼んで欲しい)場合はUniTask.Voidを併用しましょう。

private async void Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    await UniTaskAsyncEnumerable.EveryUpdate()
        .Select((_, x) => x)
        .Take(5)
        // 非同期処理を登録できるがこのasync/awaitの結果を
        // 待たずにForEachAsyncはすぐ次のメッセージを取りに行く
        .ForEachAsync(_ => UniTask.VoidAction(async () =>
        {
            Debug.Log("before await:" + Time.frameCount);
            // ForEachAwaitAsyncはここのawaitを待つ
            await UniTask.DelayFrame(10, cancellationToken: token);
            Debug.Log("after await:" + Time.frameCount);
        }), token);

    Debug.Log("Done!");
}

UniTaskAsyncEnumerableの作り方

UniTaskAsyncEnumerableはさまざまな方法で生成することができます。

ファクトリメソッド

  • Return
  • Repeat
  • Empty
  • Throw
  • Never
  • Range
  • EveryUpdate
  • Timer
  • TimerFrame
  • Interval
  • IntervalFrame

他のデータ構造から変換

  • IEnumerable<T>
  • IObservable<T>
  • Task<T>
  • UniTaskT>

uGUIコンポーネントから変換

uGUIイベントからの変換
public class FromUGui : MonoBehaviour
{
    [SerializeField] private Button _button;

    private void Start()
    {
        var token = this.GetCancellationTokenOnDestroy();

        // 連打防止ボタン
        // 1回ボタンを押したら2秒間無反応になる
        _button.OnClickAsAsyncEnumerable()
            .ForEachAwaitWithCancellationAsync(async (_, ct) =>
            {
                Debug.Log("Clicked!");
                await UniTask.Delay(2000, cancellationToken: ct);
            }, token);
    }
}

AsyncTriggerを利用する

this.GetAsyncCollisionEnterTrigger()など。

Channel

ChannelGoにおけるChannelと同義です。
挙動としてはObservableにおけるSubjectに相当します。

なお、Channelは内部でメッセージをキューイングします。

Channelの例
 private void Start()
{
    // Channel作成
    var channel = Channel.CreateSingleConsumerUnbounded<int>();

    // Channelを読み取るときはReaderを使う
    var reader = channel.Reader;

    // メッセージの待受
    WaitForChannelAsync(reader, this.GetCancellationTokenOnDestroy()).Forget();

    // 書き込むときはWriteを使う
    var writer = channel.Writer;

    // IObserver<T>.OnNext() に相当
    writer.TryWrite(1);
    writer.TryWrite(2);
    writer.TryWrite(3);

    // IObserver<T>.OnCompleted() に相当
    writer.TryComplete();

    // TryComplete()に例外を渡すと IObserver<T>.OnError() に相当
    // writer.TryComplete(new Exception(""));
}

private async UniTaskVoid WaitForChannelAsync(ChannelReader<int> reader, CancellationToken token)
{
    try
    {
        // 1回だけ読み取るならReadAsync
        var result1 = await reader.ReadAsync(token); // UniTask<int>
        Debug.Log(result1);

        // 完了するまで読み続けるなら ReadAllAsync
        // IObservable<T>.Subscribe() に相当
        await reader.ReadAllAsync() // IUniTaskAsyncEnumerable<int>
            .ForEachAsync(x => Debug.Log(x), token);

        Debug.Log("Done");
    }
    catch (Exception e)
    {
        Debug.LogException(e);
    }
}

AsyncReactiveProperty

AsyncReactivePropertyUniRxReactivePropertyUniTask版です。
ベースとしてIUniTaskAsyncEnumerable<T>が利用されています。
ReactivePropertyはベースがIObservable<T>

基本的な使い方はReactivePropertyと変わりません。

AsyncReactiveProperty
private void Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    // AsyncReactiveProperty生成
    var asyncReactiveProperty = new AsyncReactiveProperty<string>(null);

    // 待受開始
    WaitForAsync(asyncReactiveProperty, token).Forget();

    // Valueプロパティに値をセットすると
    // MoveNextAsync() が次に進む
    asyncReactiveProperty.Value = "Hello!";

    // Dispose()するとこのAsyncReactivePropertyが完了する
    asyncReactiveProperty.Dispose();
}

private async UniTaskVoid WaitForAsync(
    IReadOnlyAsyncReactiveProperty<string> asyncReadOnlyReactiveProperty,
    CancellationToken token)
{
    // Valueプロパティで現在値を取得可能
    var current = asyncReadOnlyReactiveProperty.Value;
    Debug.Log(current);

    // IUniTaskAsyncEnumerable<T>として扱える
    var result = await asyncReadOnlyReactiveProperty
        // null以外の値がセットされるのを待つ
        .FirstOrDefaultAsync(x => x != null, cancellationToken: token);

    Debug.Log(result);
}

UniTask.Linq

IUniTaskAsyncEnumerable<T>にはLINQメソッドが用意されています。
使えるメソッドは次です。

同期型

  • AggregateAsync
  • AllAsync
  • AnyAsync
  • AsUniTaskAsyncEnumerable
  • AverageAsync
  • Buffer
  • Append
  • Prepend
  • Cast
  • Concat
  • ContainsAsync
  • CountAsync
  • DefaultIfEmpty
  • Distinct
  • DistinctUntilChanged
  • Do
  • ElementAtAsync
  • ElementAtOrDefaultAsync
  • Except
  • FirstAsync
  • FirstOrDefaultAsync
  • ForEachAsync
  • GroupBy
  • GroupJoin
  • Intersect
  • Join
  • LastAsync
  • LastOrDefaultAsync
  • LongCountAsync
  • MaxAsync
  • MinAsync
  • OfType
  • OrderBy
  • OrderByDescending
  • Reverse
  • Select
  • SelectMany
  • SequenceEqualAsync
  • SingleAsync
  • SingleOrDefaultAsync
  • Skip
  • SkipLast
  • SkipWhile
  • SumAsync
  • Take
  • TakeLast
  • TakeWhile
  • ToArrayAsync
  • ToDictionaryAsync
  • ToHashSetAsync
  • ToListAsync
  • ToLookupAsync
  • ToObservable
  • Union
  • Where
  • Zip
  • Queue
  • SkipUntilCanceled
  • TakeUntilCanceled

非同期型

AwaitAsyncがついているものはasync/awaitを内部で利用できます。

  • AggregateAwaitAsync
  • AllAwaitAsync
  • AnyAwaitAsync
  • AverageAwaitAsync
  • CountAwaitAsync
  • DistinctAwait
  • DistinctUntilChangedAwait
  • DoAwait
  • FirstAwaitAsync
  • FirstOrDefaultAwaitAsync
  • ForEachAwaitAsync
  • GroupByAwait
  • GroupJoinAwait
  • JoinAwait
  • LastAwaitAsync
  • LastOrDefaultAwaitAsync
  • LongCountAwaitAsync
  • MaxAwaitAsync
  • MinAwaitAsync
  • OrderByAwait
  • OrderByDescendingAwait
  • SelectAwait
  • SelectManyAwait
  • SingleAwaitAsync
  • SingleOrDefaultAwaitAsync
  • SkipWhileAwait
  • SumAwaitAsync
  • TakeWhileAwait
  • ToDictionaryAwaitAsync
  • ToLookupAwaitAsync
  • WhereAwait
  • ZipAwait

こちらはCancellationTokenを内部で必要とするときに使います。

  • AggregateAwaitWithCancellationAsync
  • AnyAwaitWithCancellationAsync
  • AverageAwaitWithCancellationAsync
  • CountAwaitWithCancellationAsync
  • DistinctAwaitWithCancellation
  • DistinctUntilChangedAwaitWithCancellation
  • DoAwaitWithCancellation
  • FirstAwaitWithCancellationAsync
  • FirstOrDefaultAwaitWithCancellationAsync
  • ForEachAwaitWithCancellationAsync
  • GroupByAwaitWithCancellation
  • GroupJoinAwaitWithCancellation
  • JoinAwaitWithCancellation
  • LastAwaitWithCancellationAsync
  • LastOrDefaultAwaitWithCancellationAsync
  • LongCountAwaitWithCancellationAsync
  • MaxAwaitWithCancellationAsync
  • MinAwaitWithCancellationAsync
  • OrderByAwaitWithCancellation
  • OrderByDescendingAwaitWithCancellation
  • SelectAwaitWithCancellation
  • SelectManyAwaitWithCancellation
  • SingleAwaitWithCancellationAsync
  • SingleOrDefaultAwaitWithCancellationAsync
  • SkipWhileAwaitWithCancellation
  • SumAwaitWithCancellationAsync
  • TakeWhileAwaitWithCancellation
  • ToDictionaryAwaitWithCancellationAsync
  • ToLookupAwaitWithCancellationAsync
  • WhereAwaitWithCancellation
  • ZipAwaitWithCancellation

補足: PushベースのUniTaskAsyncEnumerableの注意点

IUniTaskAsyncEnumerable<T>Pull型として動作します。
そのためメッセージの受信準備が整い、内部でイテレータのMoveNextAsync()が実行されたタイミングで次のメッセージを取りに行きます。

ですが、UniTaskAsyncEnumerableではメッセージ発行がPushIUniTaskAsyncEnumerable<T>も存在します(ややこしい)

  • UniTask.EveryUpdate()
  • AsyncReactiveProperty
  • AsyncTriggerより生成したもの
  • uGUIのイベントなどから変換したもの

これらはPushされてきたイベントをIUniTaskAsyncEnumerable<T>として提供します。
そのためMoveNextAsync()とタイミングが合わなかった場合は、その間に発行されたイベントは取りこぼされるという点に注意が必要です。

image.png

逆に、このイベントの取りこぼしを利用して処理を組むこともできます。

イベントの取りこぼしを利用した例
// uGUIのボタンの連打防止
// 1回ボタンを押したら2秒間無反応になる

// ForEachAwaitAsyncが待機中に発行されたメッセージは無視するという
// 性質を利用している
_button.OnClickAsAsyncEnumerable()
    .ForEachAwaitWithCancellationAsync(async (_, ct) =>
    {
        Debug.Log("Clicked!");
        await UniTask.Delay(2000, cancellationToken: ct);
    }, token);

取りこぼしが嫌なら Queue() を使おう

QueueUniTask.Linqが提供するLINQメソッドの1つです。

IUniTaskAsyncEnumerable<T>に対して先にMoveNextAsync()を実行し、その結果をキューに詰めて再度IUniTaskAsyncEnumerable<T>として提供します。
ObservableでいうところのPublish()に相当します)

Queueを使えばイベントの取りこぼしを防ぐことができるため、必要に応じて利用しましょう。

Queue
private void Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    // AsyncReactiveProperty生成
    var asyncReactiveProperty = new AsyncReactiveProperty<string>("Initialize!");

    // 待受開始
    WaitForAsync(asyncReactiveProperty, token).Forget();

    // 値を設定
    asyncReactiveProperty.Value = "Hello!";
    asyncReactiveProperty.Value = "World!";
    asyncReactiveProperty.Value = "Thank you!";

    asyncReactiveProperty.Dispose();
}

private async UniTaskVoid WaitForAsync(
    IReadOnlyAsyncReactiveProperty<string> asyncReadOnlyReactiveProperty,
    CancellationToken token)
{
    // AsyncReactiveProperty はキューイングしてくれないので
    // タイミングによってはメッセージの取りこぼしが置きうる
    // 取りこぼしが嫌ならQueueを併用する
    await asyncReadOnlyReactiveProperty
        .Queue() // Queueを挟む
        .ForEachAwaitWithCancellationAsync(async (x, ct) =>
        {
            Debug.Log(x);
            // 1秒待って次の値を取りに行く
            await UniTask.Delay(1000, cancellationToken: ct);
        }, token);
}

まとめ

C# 8.0IAsyncEnumerable相当の処理をいち早くUnityでも利用できるため、かなり期待が高いアップデートとなりました!
UniTaskAsyncEnumerableAsyncReactivePropertyが刺さる人にはかなり刺さる機能だと思います。(自分はさっそく使いたい)

ただ、これの登場によってIObservable<T>との使い分けが一層ややこしくなったとも思います。
UniRxで苦戦していた人はおそらくUniTaskAsyncEnumerableでも苦戦するような予感が…。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】リバーシやテトリスとかに使える簡単なコンソール描画

目的

コンソール画面でアルゴリズムを視覚化するための描画処理の作成

目次

  1. 目標
  2. 作成
  3. 完成
  4. 応用
  5. まとめ

目標

  • コンソール画面に四角形を複数表示ができる
  • 縦と横の四角形の数を指定できる
  • 並べた四角形の色を自由に設定できる

作成

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;

/// <summary>
/// コンソール描画
/// </summary>
namespace ConsoleDrawer
{
    /// <summary>
    /// 四角形描画処理
    /// </summary>
    public class SquareDrawer
    {
        private ConsoleColor[,] canvas;
        private int width;
        private int height;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="width">幅</param>
        /// <param name="height">高さ</param>
        public SquareDrawer(int width, int height)
        {
            Console.CursorVisible = false;
            Console.BackgroundColor = ConsoleColor.Black;
            this.width = width;
            this.height = height;
            canvas = new ConsoleColor[height, width];

            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    canvas[i, j] = ConsoleColor.Black;
                }
            }
        }

        /// <summary>
        /// 四角形描画
        /// </summary>
        /// <param name="color">文字色</param>
        private void DrawSquare(ConsoleColor color)
        {
            Console.ForegroundColor = color;
            Console.Write("■");
        }

        /// <summary>
        /// すべての四角形の描画
        /// </summary>
        public void DrawAllSquare()
        {
            Console.ForegroundColor = ConsoleColor.Black;
            Console.SetCursorPosition(0, 0);

            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    DrawSquare(canvas[i, j]);
                }
                Console.Write("\n");
            }
        }

        /// <summary>
        /// 文字色の指定
        /// </summary>
        /// <param name="color">文字色</param>
        /// <param name="position">位置</param>
        public void SetColor(ConsoleColor color, Point position)
        {
            canvas[position.Y, position.X] = color;
        }

        /// <summary>
        /// 文字色の指定
        /// </summary>
        /// <param name="color"></param>
        /// <param name="x">横の位置</param>
        /// <param name="y">縦の位置</param>
        public void SetColor(ConsoleColor color, int x, int y)
        {
            SetColor(color, new Point(x, y));
        }
    }
}

完成

using System;
using System.Threading;
using System.Drawing;
using ConsoleDrawer;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Title = "テスト";
            SquareDrawer drawer = new SquareDrawer(6, 6);

            while (true)
            {
                // アルゴリズムはこの中で動かす

                // 色を指定
                drawer.SetColor(ConsoleColor.White, 0, 1);
                drawer.SetColor(ConsoleColor.Yellow, 0, 0);
                drawer.SetColor(ConsoleColor.Blue, 5, 0);
                drawer.SetColor(ConsoleColor.Green, 5, 5);

                drawer.DrawAllSquare();
                Thread.Sleep(500);
            }
        }
    }
}

描画するとこんな感じになります。
キャプチャ.PNG

応用

  • 四角形ではなく丸に
    • リバーシにも使える
  • 任意の図形に変更
    • より広くいろいろなアルゴリズムを扱えるようになる?

まとめ

アルゴリズムを純粋に考えたい時ように今回のものをつくりました。
c#独自のものはあまり使っていないので、他の言語にも参考になればいいと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スキャンアプリケーション ITScan

1. はじめに

TwainDotNetによるスキャン - Qiitaを元に、TwainDotNetが内部で32ビットBitmapを作成する部分を24ビット以下にも対応 - Qiitaを組み合わせて、スキャンアプリケーションを作成しました。Windows用です。
BMPとPNGで保存可能です。
BTScanを参考にして、その機能の一部を作成しています。PNGの保存は.NET標準機能で行っていますが、ファイルサイズはBTScanの圧縮率0(低い)よりも小さくなりました。(自分が試したグレースケール画像で約1/2~1/3)

ITScan_20200522.png

このアプリケーションにはスキャン画像の表示機能はありませんが、画像ビューア ImageTest1 - Qiitaを監視モードで起動しておけば、リアルタイムに表示確認が可能です。

2. バイナリ

ITScan_1.03_20200522.zip 最新版

フリーウェアとします。無保証です。

3. ソース

ITScan_src_1.03_20200522.zip 最新版
GitHubリポジトリ

TwainDotNet_1.0.0_patch_8bit_support_20200522-2.zip

旧版バイナリ、ソース

C#で記述しています。Visual Studio 2019でビルド可能です。
TwainDotNetに24ビット以下への対応の改造を行っています。

4. 動作環境

.NET Framework 4.7.2以上+.NET Framework 3.5以上。
.NET Framework 4.7.2以上はWindows 10 April 2018 Update(1803)以上であればインストールされています。
ソースからリビルドすればおそらく.NET Framework 2.0以上くらいまで落とすことができます。
TwainDotNetが.NET Framework 3.5向けのため、おそらく.NET Framework 3.5以上も必要です。
32ビットアプリケーションです。
Windows 10 64ビット版で動作確認をしています。

5. 気になる点

1) スキャンが終わったらスキャナのダイアログが閉じてしまいます。TwainDotNet自体の動作のようです。BTScanのようにスキャナのダイアログを開いたままにする方法はないものか…。
2) JPEGに対応していないのは、JPEGの品質指定のUIを作るのを面倒がっているからです。
3) ImageTest1の監視モードと併用すると、ImageTest1側で頻繁に読み込み失敗が表示されます。ImageTest1側での対処を検討中です。→ImageTest1 1.13 20200522で対応しました。

6. 更新履歴

2020/05/22 1.03
1) TwainDotNetから返却されたBitmapの解放処理を追加。これにより、数ページ~十数ページ程度(スキャナや取り込みサイズによる)で連続スキャンが止まり、途中までしかファイルが保存されないのを修正。

2020/05/22 1.02
1) TwainDotNetのビット数判定を厳密に変更。(24ビット以下→32ビット以外(64ビットを含む)、8ビット以下→24ビット未満)
2) アクセス・キーを追加。
3) カウンタの初期値を0から1に変更。
4) TwainDotNetにADFを使用するフラグのセットを削除。1.00と同じ動作に戻した。

2020/05/20 1.01
1) 24ビットカラーの場合に32ビットで保存していたのを24ビットに変更。
2) TwainDotNetにADFを使用するフラグをセット(特に動作が変わっているように見えない)。→フラットベッドとADFの両方があるスキャナの場合に、毎回ADFになりそうなので、次回アップデートで削除予定。

2020/05/19 1.00
1) 初版。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

正しいプログラミングの学習方法について語る

今回は プログラミング学習について 見ていきます。 

特に独学での学習者はこれらの点を気をつけて学習したほうがいいです。

 1 完璧主義を目指してはいけない 

プログラミングの学習において、完璧主義を目指すのはほんとに自殺行為といえます。

これは学習でもそうですけど、プロダクトを作るときも同じです。 

最初は6割や7割ベースで 全然いいと思う。 

特に独学でやっている人ほど、完璧主義を目指すのはやめましょう。

 2 暗記も必要もない

学校の勉強のような、暗記も必要ないです。

覚えるのは基礎だけでいいわけで あとは

必要なときに 必要な分だけ覚えていくのがいいと思う。 

なので メソッドやライブラリなどを全部覚える必要はないです。

 3 小さい目標をつけるべし

わたしもプログラミングは全部独学で覚えたんですけど 
独学の場合だと、やはりモチベーションが続かないんですよね。 
モチベも続かないし、普通に挫折もすると思います。 

挫折しない人なんているんでしょうかね???

なので 小さいな目標を立てることが重要です。 

例えば 1日 10ページすすめるとか 

1日 1回はアウトプットするとか 

サンプルコードをやるとかね。 

因みに習慣の法則というものがありまして 

21日間 続けることが出来れば、 そのあとも継続できるようになるので 

とりあえず 3週間は続ける事が大事。

 

 4 インプットよりも アウトプットを増やしなさい

インプット学習ってみんな普通にするんですよ。 

でも アウトプットしている人はほんとに少ないです。 

みんな、 Udemyとかの動画は見ているけど 
それで終わっている人が多いからね。 

なので、アウトプットする習慣を身に付けることが最も大事。

インプットよりも、どうやってアウトプットできるかを
考えるべし。 

初心者であれば、 パイザのコードクロニクルなどが
おすすめです。

 5 とりあえず ググり癖はつけるべし

分からなければ すぐに調べる。

調べ方は色々あるけど  

目的で検索するか、 コードをコピペしていくのが基本。

で これは 誰かに質問するときも同じで 
質問するときも、 ちゃんと調べてから質問すること。 

あと 何でもかんでも、質問すればいいってわけじゃないからね。 

あんまり、質問ばっかりしていると嫌われるよん。

PS 

C#プログラミングマスター講座
発売中でーす。

只今70%OFFセールやってます。

C#が分からないと Unityもできるようにならないので 
Unityをやるひとにも おすすめです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【VisualStudio】コードジェネレータ―が働かなかった【備忘録】

はじめに

VisualStudio2019でWeb APIを作成しようとしたとき、コントローラーを作ろうと思ったら以下のエラーが出て作成ができませんでした。
スクリーンショット (105).png

これの解決方法を備忘録として書いておきます。

環境

Windows10
VisualStudio2019

1. エラーの原因

とりあえず出たエラーに書いてある文言「パッケージの復元に失敗しました。のパッケージの変更をロールバックします。」をコピペして調べましたが、どれをやっても解決せず、、、

原因は「コードジェネレータを実行中にエラーが発生しました。」の方にありました。

VisualStudioがコントローラをスキャフォールドで作成してくれる時には、
Microsoft.VisualStudio.Web.CodeGeneration.Design
というパッケージを使います。(使うものはほかにもあります。)

コントローラを作成しようとすると、パッケージマネージャであるNuGetが勝手にこれらのパッケージをダウンロードしてきてくれて、それを使ってくれます。

そのダウンロードがなぜかできていなかったのが原因でした。

2. 解決策

  1. 上部のメニューからツール→NuGetパッケージマネージャ→パッケージマネージャの設定を選択して、設定を開く

  2. パッケージソースを選択
    ここにパッケージのダウンロード元のURLが記述されているはずなのに記述されていなかったことが原因でした。

  3. 右上のプラスボタンから新規追加し、

と記述することで完了です。

これによってダウンロード元が指定され、ここからパッケージを持ってきてくれます。

さいごに

つい最近VisualStudioをインストールしたのですが、普通最初からURL指定されていませんか?
VisualStudioを使っている人が全員この作業をしたとは思えない。
原因に気づくまでにかなり時間がかかってしまいましたが、解決してよかったです。

Twitter↓
@ruemura3

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

眺めて覚える C# Xamarin Forms(14) 連絡先 参照

今回は、連絡先のアクセスがテーマです。

連絡先をアプリに読み込む締めには、許可が必要になります。

image.png

プロジェクト作成の手順

image.png

Propertiesに変更を加えます。

image.png

必要があれば書き込み許可を与えます。

マニフェストには、下記のように自動的に追加されます。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.projectcontact" android:installLocation="auto">
    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
    <application android:label="ProjectContact.Android"></application>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
</manifest>

MainActivity.csに許可のためのルーチンを追加します。①②

MainActivity.cs
using System;

using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
//許可ダイアログ用①
using Android.Support.V4.Content;
using Android.Support.V4.App;
using Android;

namespace ProjectContact.Droid
{
    [Activity(Label = "ProjectContact", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);
            // 許可ダイアログのために追加②
            if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadContacts) != (int)Permission.Granted)
                ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.ReadContacts }, 0);
            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
            LoadApplication(new App());
        }
        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
        {
            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

依存関係にNugetで追加します。

image.png

Xamarin.Forms.Contactsを追加します。

image.png

以下の例は、xamlを用いずプログラム的にデザインを作成しています。

MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace ProjectContact
{
    // Learn more about making custom code visible in the Xamarin.Forms previewer
    // by visiting https://aka.ms/xamarinforms-previewer
    [DesignTimeVisible(false)]
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
#pragma warning disable CS4014 // この呼び出しは待機されなかったため、現在のメソッドの実行は呼び出しの完了を待たずに続行されます
            GetContacs();
        }
        class MyContact
        {
            public string Name { get; set; } = "";
            public string Email { get; set; } = "";
            public string Number { get; set; } = "";
        }
        async Task GetContacs()
        {
            var contacts = await Plugin.ContactService.CrossContactService.Current.GetContactListAsync();
            var list = new List<MyContact>();
            foreach (var x in contacts)
            {
                list.Add(new MyContact
                {
                    Name = x.Name ?? "",
                    Email = x.Email ?? "",
                    Number = (x.Numbers.Count > 0) ? x.Numbers[0] : ""
                });
            }
            var tmp = new DataTemplate(() =>
            {
                var grid = new Grid() { Margin = 0, BackgroundColor = Color.Black };
                grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
                grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
                var name = new Label { TextColor = Color.White, FontSize = 18 };
                name.SetBinding(Label.TextProperty, "Name");
                var email = new Label() { TextColor = Color.White };
                email.SetBinding(Label.TextProperty, "Email");
                var number = new Label() { TextColor = Color.White };
                number.SetBinding(Label.TextProperty, "Number");
                grid.Children.Add(name, 0, 0);
                grid.Children.Add(number, 0, 1);
                grid.Children.Add(email, 1, 1);
                return new ViewCell { View = grid };
            });
            var lv = new ListView() { ItemsSource = list, ItemTemplate = tmp };
            Content = new Xamarin.Forms.ScrollView() { Margin = 1, Orientation = ScrollOrientation.Vertical, Content = lv };
        }

    }
}

実行結果

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macOS上でSQL Serverを使用してC#アプリを作成する

はじめに

この記事では、Microsoft 社が公開している Build an app using SQL Server の内容に従い、SQL Server を使用した C# アプリを作成します。

環境

  • OS: macOS Catalina バージョン 10.15.4
  • SQL Server: SQL Server 2019
  • .NET Core: 3.1 LTS

環境のセットアップ

ここでは、SQL Server を Docker 上で取得します。その後、SQL Server で .NET Core アプリを作成するために必要な依存関係をインストールします。

SQL Server のインストール

  1. macOS で SQL Server を実行するには、SQL Server on Linux の Docker イメージ を使用します。そのためには、Docker for Mac をインストールする必要があります。
  2. Docker 環境に最低 4GB のメモリを設定し、パフォーマンスを評価したい場合は複数のコアを追加することも検討します。これは、メニューバーの [環境設定] -> [詳細設定] オプションで行うことができます。
  3. 新しいターミナルプロンプトを起動し、以下のコマンドを使用して SQL Server on Linux Docker イメージをダウンロードして起動します。SA_PASSWORD の部分は特殊文字を使用した強力なパスワードを使用するように書き換えてください。
sudo docker pull microsoft/mssql-server-linux:2017-latest
docker run -e 'HOMEBREW_NO_ENV_FILTERING=1' -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -d microsoft/mssql-server-linux

筆者は、Docker Desktop をインストールすると一緒に利用可能になる、Docker Compose を利用して、SQL Server 2019 on Linux をインストールしています。docker-compose.yaml については、以下の GitHub リポジトリを参照してください。

なお、ウェブ上で公開されている Docker イメージ の URL は、Ubuntu ベースの SQL Server 2017 on Linux の Docker イメージ です。SQL Server 2019では、従来の Ubuntu ベースに加え、RHEL ベース の SQL Server on Linux の Docker イメージ もサポートされるようになりました。
Ubuntu ベースを利用するか、RHEL ベースを利用するかは、好きな方を選択してください。(SQL Server における機能に差はありません)

Homebrew と .NET Core のインストール

すでに .NET Core 3.1 LTS がインストールされている場合は、このステップをスキップしてください。

公式インストーラー をダウンロードして、.NET Coreをインストールします。インストーラーは、Build apps - SDK のものを選択してください。.NET Core 3.1.x SDK および、.NET Core ランタイムを一緒にインストールできます。

  • ASP.NET Core Runtime
  • Desktop Runtime
  • .NET Core Runtime (上の2つを一緒にしたもの)

.NET Core では、作成したアプリを実行するためのランタイムが、種類によって分けられています。
ダウンロードサイト上では分かれて表示されているため、悩んでしまう可能性がありますが、ここでは気にしないでください。

なお、本家のサイトにあるリンクは .NET Core 2.0 のダウンロードリンクになっています。これは既にサポート切れ、かつ LTS ではないため、最新の .NET Core 3.1.x をダウンロードし、インストールするようにしてください。.NET Core 3.1 は LTS バージョンになります。

なお、筆者は、SQL Server 2019 と同様、Docker Compose を使用して、.NET Core 3.1 のコンテナーを作成し、開発を進めています。docker-compose.yaml については、以下の GitHub リポジトリを参照してください。

SQL Server および .NET Core 3.1 を Docker 上で利用する場合は、同じ docker-compose.yaml ファイル内に記述します。container_name を記述することで、コンテナ名を使ってコンテナ同士の相互通信が可能になります。

docker-compose.yaml(例)
version: '3'

services:
  app:
    image: mcr.microsoft.com/dotnet/core/sdk:latest
    container_name: dotnetcoreapp
    tty: true
    ports:
      - 10080:80
    volumes:
      - ./src:/src
    working_dir: "/src"

  mssql:
    image: mcr.microsoft.com/mssql/rhel/server:2019-latest
    container_name: 'mssql2019'
    environment:
      - MSSQL_SA_PASSWORD=databaseadmin@1
      - ACCEPT_EULA=Y
    ports:
      - 1433:1433
    # volumes: # Mounting a volume does not work on Docker for Mac
    #   - ./mssql/log:/var/opt/mssql/log
    #   - ./mssql/data:/var/opt/mssql/data

SQL Server を使った C# アプリケーションを作成

ここでは、以下、2 つのシンプルな C# アプリを作成します。

  • 基本的な Insert、Update、Delete、Select を実行するアプリ
  • .NET Core の ORM フレームワークの中でも特に人気のある Entity Framework Core を利用してInsert、Update、Delete、Select を実行するアプリ

SQL Server に接続してクエリを実行する C# アプリを作成

開発を行うワークディレクトリに移動し、新しい .NET Core プロジェクトを作成します。
基本的な .NET Core の Program.cs と csproj ファイルを含むプロジェクトディレクトリが作成されます。

cd ~/
dotnet new console -o SqlServerSample

Docker Compose で行う場合は、以下の通りです。

docker-compose run --rm app dotnet new console -o SqlServerSample

SqlServerSample.csproj というファイルが SqlServerSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerSample.csproj ファイルを開き、コードを以下の通りに書き換え、System.Data.SqlClient をプロジェクトに追加します。保存してファイルを閉じます。

SqlServerSample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
  </ItemGroup>

</Project>

SqlServerSample ディレクトリ以下にある Program.cs ファイルを開き、コードを以下の通りに書き換え、保存してファイルを閉じます。
ユーザー名とパスワードを自分のものに置き換えることを忘れないでください。

Program.cs
using System;
using System.Data.SqlClient;

namespace SqlServerSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                // 接続文字列の構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";   // 接続先の SQL Server インスタンス
                builder.UserID = "sa";              // 接続ユーザー名
                builder.Password = "your_password"; // 接続パスワード
                builder.InitialCatalog = "master";  // 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;  // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                // SQL Server に接続
                Console.Write("SQL Server に接続しています... ");
                using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
                {
                    connection.Open();
                    Console.WriteLine("接続成功。");
                }
            }
            catch (SqlException e)
            {
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("すべてが完了しました。任意のキーを押してアプリを終了します...");
            Console.ReadKey(true);
        }
    }
}

SqlServerSample ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。

cd ~/SqlServerSample
dotnet restore

完了したら、ビルド実行を行います。

dotnet run

なお、Docker Compose で実行している場合は、以下のようなコマンドを実行することで上記を実現できます。

docker-compose run -w /src/SqlServerSample --rm app dotnet restore
docker-compose run -w /src/SqlServerSample --rm app dotnet run

SqlServerSample1.gif

これで、SQL Server に接続を行うコンソールアプリができました。ただし、このアプリでは単にデータベースへの接続だけを行っているだけで、クエリは実行していません。
次に、Program.cs 内にコードを追加して、データベースやテーブルの作成、INSERT/UPDATE/DELETE/SELECT などのクエリを実行するように変更します。
ユーザー名とパスワードは自分のものに置き換えることを忘れないでください。
書き換えた後、ファイルを保存し、プロジェクトをビルドして実行します。

Program.cs
using System;
using System.Text;
using System.Data.SqlClient;

namespace SqlServerSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("SQL Server に接続し、Create、Read、Update、Delete 操作のデモを行います。");

                // 接続文字列の構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";   // 接続先の SQL Server インスタンス
                builder.UserID = "sa";              // 接続ユーザー名
                builder.Password = "your_password"; // 接続パスワード
                builder.InitialCatalog = "master";  // 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;  // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                // SQL Server に接続
                Console.Write("SQL Server に接続しています... ");
                using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
                {
                    connection.Open();
                    Console.WriteLine("接続成功。");

                    // サンプルデータベースの作成
                    Console.Write("既に作成されている SampleDB データベースを削除し、再作成します... ");
                    String sql = "DROP DATABASE IF EXISTS [SampleDB]; CREATE DATABASE [SampleDB]";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("SampleDB データベースを作成しました。");
                    }

                    // テーブルを作成しサンプルデータを登録
                    Console.Write("サンプルテーブルを作成しデータを登録します。任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    StringBuilder sb = new StringBuilder();
                    sb.Append("USE SampleDB; ");
                    sb.Append("CREATE TABLE Employees ( ");
                    sb.Append(" Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, ");
                    sb.Append(" Name NVARCHAR(50), ");
                    sb.Append(" Location NVARCHAR(50) ");
                    sb.Append("); ");
                    sb.Append("INSERT INTO Employees (Name, Location) VALUES ");
                    sb.Append("(N'Jared', N'Australia'), ");
                    sb.Append("(N'Nikita', N'India'), ");
                    sb.Append("(N'Tom', N'Germany'); ");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("作成完了");
                    }

                    // INSERT デモ
                    Console.Write("テーブルに新しい行を挿入するには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sb.Clear();
                    sb.Append("INSERT Employees (Name, Location) ");
                    sb.Append("VALUES (@name, @location);");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.Parameters.AddWithValue("@name", "Jake");
                        command.Parameters.AddWithValue("@location", "United States");
                        int rowsAffected = command.ExecuteNonQuery();
                        Console.WriteLine(rowsAffected + " 行 挿入されました");
                    }

                    // UPDATE デモ
                    String userToUpdate = "Nikita";
                    Console.Write("ユーザー '" + userToUpdate + "' の 'Location' を更新するには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sb.Clear();
                    sb.Append("UPDATE Employees SET Location = N'United States' WHERE Name = @name");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.Parameters.AddWithValue("@name", userToUpdate);
                        int rowsAffected = command.ExecuteNonQuery();
                        Console.WriteLine(rowsAffected + " 行 更新されました");
                    }

                    // DELETE デモ
                    String userToDelete = "Jared";
                    Console.Write("ユーザー '" + userToDelete + "' を削除するには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sb.Clear();
                    sb.Append("DELETE FROM Employees WHERE Name = @name;");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.Parameters.AddWithValue("@name", userToDelete);
                        int rowsAffected = command.ExecuteNonQuery();
                        Console.WriteLine(rowsAffected + " 行 削除されました");
                    }

                    // READ デモ
                    Console.WriteLine("テーブルからデータを読み取るには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sql = "SELECT Id, Name, Location FROM Employees;";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        using (SqlDataReader reader = command.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                Console.WriteLine("{0} {1} {2}", reader.GetInt32(0), reader.GetString(1), reader.GetString(2));
                            }
                        }
                    }
                }
            }
            catch (SqlException e)
            {
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("すべて完了しました。任意のキーを押して終了します...");
            Console.ReadKey(true);
        }
    }
}

SqlServerSample2.gif

これで、macOS 上の .NET Core を使って、初めて C# + SQL Server アプリを作成できました。次は、ORM を使って C# アプリを作成します。

.NET Core で Entity Framework Core ORM を使用して SQL Server に接続する C# アプリを作成

ワークディレクトリに戻り、新しい.NET Coreプロジェクトを作成します。

cd ~/
dotnet new console -o SqlServerEFSample

Docker Compose で行う場合は、以下の通りです。

docker-compose run --rm app dotnet new console -o SqlServerSampleEF

SqlServerEFSample.csproj というファイルが SqlServerEFSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerEFSample.csproj ファイルを開き、コードを以下の通りに書き換え、Entity Framework Core をプロジェクトに追加します。保存してファイルを閉じます。

SqlServerEFSample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.4" />
  </ItemGroup>

</Project>

このサンプルでは、2つのテーブルを作成します。1つ目は「ユーザー」に関するデータを保持し、もう1つは「タスク」に関するデータを保持するものです。

User.cs を作成します。

User クラスを定義します。SqlServerEFSample ディレクトリ以下に User.cs ファイルを作成します。
このクラスは、User テーブルに紐づくモデルのクラスです。書き換えた後、ファイルを保存して閉じます。
この時点では、Task クラスがないためコンパイルエラーとなりますが、問題ありません。

User.cs
using System;
using System.Collections.Generic;

namespace SqlServerEFSample
{
    public class User
    {
        public int UserId { get; set; }
        public String FirstName { get; set; }
        public String LastName { get; set; }
        public virtual IList<Task> Tasks { get; set; }

        public String GetFullName()
        {
            return this.FirstName + " " + this.LastName;
        }
        public override string ToString()
        {
            return "User [id=" + this.UserId + ", name=" + this.GetFullName() + "]";
        }
    }
}

Task.cs を作成します。

Task クラスを定義します。SqlServerEFSample ディレクトリ以下に Task.cs ファイルを作成します。
このクラスは、Task テーブルに紐づくモデルのクラスです。書き換えた後、ファイルを保存して閉じます。

Task.cs
using System;

namespace SqlServerEFSample
{
    public class Task
    {
        public int TaskId { get; set; }
        public string Title { get; set; }
        public DateTime DueDate { get; set; }
        public bool IsComplete { get; set; }
        public virtual User AssignedTo { get; set; }

        public override string ToString()
        {
            return "Task [id=" + this.TaskId + ", title=" + this.Title + ", dueDate=" + this.DueDate.ToString() + ", IsComplete=" + this.IsComplete + "]";
        }
    }
}

EFSampleContext.cs を作成します。

EFSampleContext クラスを定義します。SqlServerEFSample ディレクトリ以下に EFSampleContext.cs ファイルを作成します。
このクラスは、Entity Framework Core を使用し、.NET オブジェクトを利用してデータのクエリ、挿入、更新、および削除を行うためのクラスです。User クラスと Task クラスを使用しています。
書き換えた後、ファイルを保存して閉じます。

EFSampleContext.cs
using Microsoft.EntityFrameworkCore;

namespace SqlServerEFSample
{
    public class EFSampleContext : DbContext
    {
        string _connectionString;
        public EFSampleContext(string connectionString)
        {
            this._connectionString = connectionString;
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(this._connectionString);
        }

        public DbSet<User> Users { get; set; }
        public DbSet<Task> Tasks { get; set; }
    }
}

Entity Framework (.NET Framework) と違う点としては、OnConfiguring メソッドが新たにオーバーライドされ、逆に Database.SetInitializer(IDatabaseInitializer) を EFSampleContext のコンストラクタ内で指定しなくなっている点です。

最後に Program.cs を更新します。これまで作成したクラスを使用するための設定を行います。
ユーザー名とパスワードを自分のものに更新することを忘れないでください。
保存してファイルを閉じます。

Program.cs
using System;
using System.Linq;
using System.Data.SqlClient;
using System.Collections.Generic;

namespace SqlServerEFSample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("** Entity Framework Core と SQL Server を使用した C# CRUD のサンプル **\n");
            try
            {
                // 接続文字列を構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";     // 接続先の SQL Server インスタンス
                builder.UserID = "sa";                // 接続ユーザー名
                builder.Password = "your_password";   // 接続パスワード
                builder.InitialCatalog = "EFSampleDB";// 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;    // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                using (EFSampleContext context = new EFSampleContext(builder.ConnectionString))
                {
                    context.Database.EnsureDeleted();
                    context.Database.EnsureCreated();
                    Console.WriteLine("C#のクラスからデータベーススキーマを作成しました。");

                    // Create デモ: ユーザーインスタンスを作成し、データベースに保存
                    User newUser = new User { FirstName = "Anna", LastName = "Shrestinian" };
                    context.Users.Add(newUser);
                    context.SaveChanges();
                    Console.WriteLine("\n作成されたユーザー: " + newUser.ToString());

                    // Create デモ: タスクインスタンスを作成し、データベースに保存
                    Task newTask = new Task() { Title = "Ship Helsinki", IsComplete = false, DueDate = DateTime.Parse("04-01-2017") };
                    context.Tasks.Add(newTask);
                    context.SaveChanges();
                    Console.WriteLine("\nCreated Task: " + newTask.ToString());

                    // Association demo: Assign task to user
                    newTask.AssignedTo = newUser;
                    context.SaveChanges();
                    Console.WriteLine("\n作成されたタスク: '" + newTask.Title + "' 割り当てられたユーザー: '" + newUser.GetFullName() + "'");

                    // Read デモ: ユーザー 'Anna' に割り当てられた未完了のタスクを見つける
                    Console.WriteLine("\n'Anna' に割り当てられた未完了のタスク:");
                    var query = from t in context.Tasks
                                where t.IsComplete == false &&
                                t.AssignedTo.FirstName.Equals("Anna")
                                select t;
                    foreach(var t in query)
                    {
                        Console.WriteLine(t.ToString());
                    }

                    // Update デモ: タスクの '期限' を変更
                    Task taskToUpdate = context.Tasks.First(); // 最初のタスクを取得
                    Console.WriteLine("\nタスクをアップデート中: " + taskToUpdate.ToString());
                    taskToUpdate.DueDate = DateTime.Parse("06-30-2016");
                    context.SaveChanges();
                    Console.WriteLine("変更された期限: : " + taskToUpdate.ToString());

                    // Delete デモ: 2016年が期限になっているすべてのタスクを削除
                    Console.WriteLine("\n期限が2016年になっているすべてのタスクを削除します。");
                    DateTime dueDate2016 = DateTime.Parse("12-31-2016");
                    query = from t in context.Tasks
                            where t.DueDate < dueDate2016
                            select t;
                    foreach(Task t in query)
                    {
                        Console.WriteLine("Deleting task: " + t.ToString());
                        context.Tasks.Remove(t);
                    }
                    context.SaveChanges();

                    // 'Delete' 操作の後にタスクを表示 - 0個のタスクがあるはず
                    Console.WriteLine("\n削除後のタスク:");
                    List<Task> tasksAfterDelete = (from t in context.Tasks select t).ToList<Task>();
                    if (tasksAfterDelete.Count == 0)
                    {
                        Console.WriteLine("[なし]");
                    }
                    else
                    {
                        foreach (Task t in query)
                        {
                            Console.WriteLine(t.ToString());
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("すべて完了しました。任意のキーを押して終了します...");
            Console.ReadKey(true);
        }
    }
}

EFSampleContext クラスのコンストラクタで Database.SetInitializer(IDatabaseInitializer) を行っていないため、Program.cs 内で context.Database.EnsureDeleted() と context.Database.EnsureCreated() が行われていますね。

SqlServerSampleEF ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。

cd ~/SqlServerEFSample
dotnet restore

完了したら、ビルド実行を行います。

dotnet run

Docker Compose で実行する場合は、以下のコマンドを実行してください。

docker-compose run -w /src/SqlServerEFSample --rm app dotnet restore
docker-compose run -w /src/SqlServerEFSample --rm app dotnet run

SqlServerEFSample.gif

これで、2つ目の C# アプリの作成が終わりました。最後に、SQL Server の カラムストア機能を使って C# アプリを高速化する方法について学びます。

C# アプリを 100 倍速にする

これまでで基本的なことは理解できたと思います。最後は、SQL Server を使用してアプリをより良くする方法を見てみます。このモジュールでは、カラムストアインデックスの簡単な例と、カラムストアインデックスがどのようにデータ処理速度を向上させるかを確認します。カラムストアインデックスは、従来の列ストアインデックスに比べて、分析ワークロードでは最大 100 倍のパフォーマンス向上、データ圧縮では最大 10 倍のパフォーマンス向上を実現できます。

カラムストアインデックスの機能を確認するために、500 万行のサンプルデータベースとサンプルテーブルを作成し、カラムストアインデックスを追加する前と後の簡単なクエリを実行する C# アプリケーションを作成します。

ワークディレクトリに戻り、新しい.NET Coreプロジェクトを作成します。

cd ~/
dotnet new console -o SqlServerColumnstoreSample

Docker Compose で行う場合は、以下の通りです。

docker-compose run --rm app dotnet new console -o SqlServerColumnstoreSample

SqlServerColumnstoreSample.csproj というファイルが SqlServerColumnstoreSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerColumnstoreSample.csproj ファイルを開き、コードを以下の通りに書き換え、System.Data.SqlClient をプロジェクトに追加します。保存してファイルを閉じます。

SqlServerColumnstoreSample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
  </ItemGroup>

</Project>

Program.cs の内容を書き換えます。
ユーザー名とパスワードは自分のものに置き換えることを忘れないでください。
保存してファイルを閉じます。

Program.cs
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SqlServerColumnstoreSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("*** SQL Server カラムストアのデモ ***");

                // 接続文字列の構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";   // 接続先の SQL Server インスタンス
                builder.UserID = "sa";              // 接続ユーザー名
                builder.Password = "your_password"; // 接続パスワード
                builder.InitialCatalog = "master";  // 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;  // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                // SQL Server に接続
                Console.Write("SQL Serverへ接続しています... ");
                using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
                {
                    connection.Open();
                    Console.WriteLine("接続完了。");

                    // サンプルデータベースの作成
                    Console.Write("'SampleDB' を再作成しています... ");
                    String sql = "DROP DATABASE IF EXISTS [SampleDB]; CREATE DATABASE [SampleDB]";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("完了。");
                    }

                    // 'Table_with_5M_rows' テーブルに500万行を挿入
                    Console.Write("テーブル 'Table_with_5M_rows' に500万行を挿入します。1分ほどかかりますが、お待ちください... ");
                    StringBuilder sb = new StringBuilder();
                    sb.Append("USE SampleDB; ");
                    sb.Append("WITH a AS (SELECT * FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS a(a))");
                    sb.Append("SELECT TOP(5000000)");
                    sb.Append("ROW_NUMBER() OVER (ORDER BY a.a) AS OrderItemId ");
                    sb.Append(",a.a + b.a + c.a + d.a + e.a + f.a + g.a + h.a AS OrderId ");
                    sb.Append(",a.a * 10 AS Price ");
                    sb.Append(",CONCAT(a.a, N' ', b.a, N' ', c.a, N' ', d.a, N' ', e.a, N' ', f.a, N' ', g.a, N' ', h.a) AS ProductName ");
                    sb.Append("INTO Table_with_5M_rows ");
                    sb.Append("FROM a, a AS b, a AS c, a AS d, a AS e, a AS f, a AS g, a AS h;");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("完了。");
                    }

                    // カラムストアインデックスなしで SQL クエリを実行
                    double elapsedTimeWithoutIndex = SumPrice(connection);
                    Console.WriteLine("カラムストアインデックスなしのクエリ時間: " + elapsedTimeWithoutIndex + "ms");

                    // カラムストアインデックスを追加
                    Console.Write("'Table_with_5M_rows' テーブルにカラムストアインデックスを追加中... ");
                    sql = "CREATE CLUSTERED COLUMNSTORE INDEX columnstoreindex ON Table_with_5M_rows;";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("完了。");
                    }

                    // カラムストアインデックスが追加された後、再度同じ SQL クエリを実行
                    double elapsedTimeWithIndex = SumPrice(connection);
                    Console.WriteLine("カラムストアありのクエリ時間: " + elapsedTimeWithIndex + "ms");

                    // カラムストアインデックスの追加によるパフォーマンス向上を計算
                    Console.WriteLine("カラムストアインデックスによる性能向上: "
                        + Math.Round(elapsedTimeWithoutIndex / elapsedTimeWithIndex) + "x!");
                }
                Console.WriteLine("すべて完了しました。任意のキーを押して終了します...");
                Console.ReadKey(true);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        public static double SumPrice(SqlConnection connection)
        {
            String sql = "SELECT SUM(Price) FROM Table_with_5M_rows";
            long startTicks = DateTime.Now.Ticks;
            using (SqlCommand command = new SqlCommand(sql, connection))
            {
                try
                {
                    command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                    var sum = command.ExecuteScalar();
                    TimeSpan elapsed = TimeSpan.FromTicks(DateTime.Now.Ticks) - TimeSpan.FromTicks(startTicks);
                    return elapsed.TotalMilliseconds;
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }
            }
            return 0;
        }
    }
}

SqlServerColumnstoreSample ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。

cd ~/SqlServerColumnstoreSample
dotnet restore

完了したら、ビルド実行を行います。

dotnet run

Docker Compose で実行する場合は、以下のコマンドを実行してください。

docker-compose run -w /src/SqlServerColumnstoreSample --rm app dotnet restore
docker-compose run -w /src/SqlServerColumnstoreSample --rm app dotnet run

SqlServerColumnStoreSample.gif

おめでとうございます。カラムストアインデックスを使って C# アプリを高速化しました!

おわりに

以上で、「macOS上でSQL Serverを使用してC#アプリを作成する」は終了です。Build an app using SQL Server には、他言語での SQL Server アプリを作成するチュートリアルがあります。ぜひ、他の言語でも試してみてください。


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xamarin Android 各要素の高さが定まっていないGridViewのCustomRenderer作成

前書き

GridViewのカスタムレンダラー化は過去記事をご覧ください。
Xamarin Android GridViewのカスタムレンダラーの作成

今回は上記の記事で既にカスタムレンダラー化されたGridViewがある前提で、
そのGridViewを各要素の高さが定まっていないGridViewにカスタマイズしていきます。

やりたいこと

具体的に私の場合は、人(要素)に対して複数件のデータが紐づいているような一覧表示をしようとしました。
【完成系】
Screenshot_20200515-022507.png

実装方法

GridViewは高さの違う要素がある状態でスクロールするとGridViewが消えるようなので(実装中発見しました)
要素の高さを、行の中で一番高いものにリサイズする実装を選びました。

実装

※私のnamespaceやプロジェクト名は自分のものに置き換えてください

PLC側

using System.Runtime.CompilerServices;
using Xamarin.Forms;

[assembly: InternalsVisibleTo("MyProject.Android")]
namespace MyProject.Views.Renderer
{
    public class PCLCustomGridViewRenderer : ItemsView
    {
    }
}

XamlからPCLCustomGridViewRendererを呼び出します

<StackLayout xmlns:renderer="clr-namespace:MyProject.Views.Renderer">
    <renderer:PCLCustomGridViewRenderer ItemsSource="{Binding Items}"/>
</StackLayout>

バインドしてるItemsの中身です。

IList<Item> Items;

public class Item
{
    public string Name { get; set; }
    public ObservableCollection<ItemDetail> Details { get; set; }
}

public class ItemDetail
{
   public string Text { get; set; }
}

Xamarin.Android側

AndroidCustomGridViewRenderer.cs

using Android.Content;
using Android.Support.V7.View;
using Android.Widget;
using MyProject.Droid.Renderer;
using MyProject.Droid.Views;
using MyProject.Droid.Views.Adapter;
using MyProject.Views.Renderer;
using System.ComponentModel;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

//互いに参照できるよう定義
[assembly: ExportRenderer(typeof(PCLCustomGridViewRenderer), typeof(AndroidCustomGridViewRenderer))]
namespace MyProject.Droid.Renderer
{
    public class AndroidCustomGridViewRenderer : ViewRenderer<PCLCustomGridViewRenderer, GridView>
    {
        private AndroidCustomGridViewAdapter adapter;
        public AndroidCustomGridViewRenderer(Context context) : base(context) { }

        // 生成時一度だけ呼ばれるイベント
        protected override void OnElementChanged(ElementChangedEventArgs<PCLCustomGridViewRenderer> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null)
            {
                if (adapter != null)
                    adapter.Element = null;
            }

            if (e.NewElement != null)
            {
                if (Control == null)
                {
                    // Adpterの生成
                    adapter = new AndroidCustomGridViewAdapter(Context);

                    // GridViewの生成
                    var gridView = new CustomGridView(new ContextThemeWrapper(Context, Resource.Style.VerticalScrollbarRecyclerView));
                    // GridViewのパラメータを設定(親サイズに追従する設定)
                    gridView.LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent);
                    // カラム数
                    gridView.NumColumns = 3;
                    // Viewに対しAdpterを設定
                    gridView.Adapter = adapter;

                    //多分コントロールを実際に生成
                    SetNativeControl(gridView);
                }

                if (adapter != null)
                    // NewElementにはIList<Person>が入ってきます
                    // ElementはAdpterクラスが私が定義しました。
                    adapter.Element = e.NewElement;
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == ItemsView.ItemsSourceProperty.PropertyName)
            {
                adapter?.UpdateItems();
            }
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            adapter.Dispose();
        }
    }
}

CustomGridView.cs

描画前に各要素に高さの測定を指示します

using Android.Content;
using Android.Util;
using Android.Widget;
using MyProject.Droid.Views.Adapter;

namespace MyProject.Droid.Views
{
    class CustomGridView : GridView
    {
        public CustomGridView(Context context) : base(context)
        {
        }

        public CustomGridView(Context context, IAttributeSet attrs) : base(context, attrs)
        {
        }

        public CustomGridView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle)
        {
        }

        protected override void OnLayout(bool changed, int l, int t, int r, int b)
        {
            // グリッドビューの表示時に、各要素の高さを図る
            AndroidCustomGridViewAdapter adapter = (AndroidCustomGridViewAdapter)Adapter;
            // 列数と、要素数の設定
            CustomGridViewLinearLayout.initItemLayout(NumColumns, adapter.Count);
            int columnWidth = MeasuredWidth / NumColumns;
            // 各要素の高さを測定
            adapter.measureItems(columnWidth);
            base.OnLayout(changed, l, t, r, b);
        }
    }
}

AndroidCustomGridViewAdapter.cs

using System.Collections.ObjectModel;
using System.Linq;
using Android.Content;
using Android.Views;
using Android.Widget;
using MyProject.Models;
using MyProject.Views.Renderer;

namespace MyProject.Droid.Views.Adapter
{
    class AndroidCustomGridViewAdapter : BaseAdapter
    {
        private PCLCustomGridViewRenderer element;
        public PCLCustomGridViewRenderer Element
        {
            get => element;
            set
            {
                element = value;
                UpdateItems();
            }
        }

        Context context;

        private ObservableCollection<Item> viewModels = new ObservableCollection<Item>();

        public AndroidCustomGridViewAdapter(Context c)
        {
            context = c;
        }

        public override int Count
        {
            get { return viewModels.Count(); }
        }

        public override Java.Lang.Object GetItem(int position)
        {
            return null;
        }

        public override long GetItemId(int position)
        {
            return 0;
        }
        public void UpdateItems()
        {
            viewModels.Clear();

            if (Element?.ItemsSource != null)
            {
                foreach (var item in Element.ItemsSource)
                {
                    if (item is Item model)
                    {
                        viewModels.Add(model);
                    }
                }
            }

            NotifyDataSetChanged();
        }

        public override Android.Views.View GetView(int position, Android.Views.View convertView, ViewGroup parent)
        {
            var view = (CustomGridViewLinearLayout)LayoutInflater.From(parent.Context).Inflate(Resource.Layout.custom_gridview_item, parent, false);
            view.setPosition(position);
            // 要素の作成
            SetUp(view, viewModels[position]);

            return view;
        }

        private void SetUp(Android.Views.View view, Item model)
        {
            var name = view.FindViewById<TextView>(Resource.Id.name);
            var details = view.FindViewById<LinearLayout>(Resource.Id.details);

            name.Text = model.Name;
            details.RemoveAllViews();
            foreach (var detail in model.Details)
            {
                var textView = new TextView(view.Context);
                textView.Text = detail.Text;
                details.AddView(textView);
            }
        }

        /// <summary>
        /// 描画の前に各要素の高さを図ります
        /// </summary>
        /// <param name="columnWidth">列一つ分の幅</param>
        public void measureItems(int columnWidth)
        {
            LayoutInflater inflater = (LayoutInflater)context.GetSystemService(Context.LayoutInflaterService);
            CustomGridViewLinearLayout itemView = (CustomGridViewLinearLayout)inflater.Inflate(Resource.Layout.custom_gridview_item, null);

            int widthMeasureSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec(columnWidth, MeasureSpecMode.Exactly);
            int heightMeasureSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified);

            for (int index = 0; index < viewModels.Count; index++)
            {
                Item item = viewModels[index];

                // 測定用のViewを設定
                itemView.setPosition(index);
                SetUp(itemView, item);

                // 強制的にViewのOnMeasureイベントを発火
                itemView.RequestLayout();
                itemView.Measure(widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

CustomGridViewLinearLayout.cs

GridView内の1要素

using Android.Content;
using Android.Util;
using Android.Widget;

namespace MyProject.Droid.Views
{
    class CustomGridViewLinearLayout : LinearLayout
    {
        private static int[] maxRowHeight;

        private static int numColumns;

        private int position;

        public CustomGridViewLinearLayout(Context context) : base(context)
        {
        }

        public CustomGridViewLinearLayout(Context context, IAttributeSet attrs) : base(context, attrs)
        {
        }

        public void setPosition(int position)
        {
            this.position = position;
        }

        public static void initItemLayout(int numColumns, int itemCount)
        {
            CustomGridViewLinearLayout.numColumns = numColumns;
            maxRowHeight = new int[itemCount];
        }

        /// <summary>
        /// 自分の高さを計測、同じ行に自分より高い要素があれば、高さを上書き
        /// </summary>
        /// <param name="widthMeasureSpec"></param>
        /// <param name="heightMeasureSpec"></param>
        protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
        {
            base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
            if (numColumns <= 1 || maxRowHeight == null || maxRowHeight.Length == 0)
            {
                return;
            }

            int rowIndex = position / numColumns;
            int measuredHeight = MeasuredHeight;
            if (measuredHeight > maxRowHeight[rowIndex])
            {
                maxRowHeight[rowIndex] = measuredHeight;
            }
            SetMeasuredDimension(MeasuredWidth, maxRowHeight[rowIndex]);
        }
    }
}

custom_gridview_item.xml

Resources/layoutフォルダに配置してください

<MyProject.Droid.Views.CustomGridViewLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="@drawable/gridview_item_frame"
    android:padding="7dp"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <TextView
            android:id="@+id/name"
            android:singleLine="true"
            android:text="aaaa"
            android:textSize="18dp"
            android:textColor="#464646"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <LinearLayout
            android:id="@+id/details"
            android:layout_marginTop="10dp"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

</MyProject.Droid.Views.CustomGridViewLinearLayout>

gridview_item_frame.xml

Resources/drawbleフォルダに配置してください。
要素に枠を追加します。

<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

  <stroke android:width="1dp" android:color="#e8d689"/>
  <solid android:color="@android:color/white" />
  <corners android:radius="1dp" />
</shape>

custom_gridview_item_detail.xml

Resources/layoutフォルダに配置してください

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/text"
        android:text="ああああああああああああああああ"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"    
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

完成

ビルドしてみてください。

コード量が多くなってしまいましたが、ジェネリクスを使えば、もっと汎用的なクラスにできると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む