shuhelohelo’s blog

Xamarin.Forms多めです.

privateフィールドの値を取得したりprivateメソッドを実行したり

フィールドの場合はBindingFlags.GetField, メソッドの場合はBindingFlags.InvokeMethodを指定している.

GetField()メソッドやGetMethod()メソッドを使う場合は上記のフラグはそもそも必要ない.

   ClassHoge hoge = new ClassHoge();
    Type hogeType = hoge.GetType();

    //private field
    FieldInfo field = hogeType.GetField("_fieldA", BindingFlags.NonPublic | BindingFlags.Instance);
    string val = (string)field.GetValue(hoge);
        Console.WriteLine(val);
    //or

    var val2 = (string)hogeType.InvokeMember("_fieldA", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField, null, hoge, null);
        Console.WriteLine(val2);

    //private method
    var method = hogeType.GetMethod("MethodA", BindingFlags.NonPublic | BindingFlags.Instance);
    var result = method.Invoke(hoge, new object[] { 10, 20 });
        Console.WriteLine(result);

    //or

    var result2 = (string)hogeType.InvokeMember("MethodA", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, hoge, new object[] { 30, 40 });
        Console.WriteLine(result2);

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

public class ClassHoge
{
    private string _fieldA = "This is private Field";

    private string MethodA(int a, int b)
    {
        return $"This is private method: a = {a}, b = {b}";
    }
}

実行結果

This is private Field
This is private Field
This is private method: a = 10, b = 20
This is private method: a = 30, b = 40

C#のDisposeパターン

クラスを作成するときに,クラス内で使用するリソースを適切に開放するために,IDisposableインターフェイスを実装します.

IDisposableを実装するときに,以下のように「Disposeパターンを使って明示的にインターフェイスを実装します」という項目があるのでそれを使ってみると.

f:id:shuhelohelo:20201022122609p:plain

このような雛形が追加されます.(日本語部分は私がメモとして追加)

        private bool disposedValue;

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    // TODO: dispose managed state (managed objects).管理リソースの開放
                }

                // TODO: free unmanaged resources (unmanaged objects) and override finalizer
                // TODO: set large fields to null
                // 非管理リソースの開放
                // 一時ファイルなどを削除したり,大きなフィールドにnullをセットするなど

                disposedValue = true;
            }
        }

        // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources: 非管理リソースの開放が必要な場合のみ,このデストラクタを有効にする
        // ~TemporaryFile()
        // {
        //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        //     Dispose(disposing: false);
        // }

        void IDisposable.Dispose()
        {
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

と,Visual Studioで用意してくれる雛形に合わせて,それぞれ管理リソースと非管理リソースの開放処理を記述すると良いです.

IDisposableについての詳しい解説は以下の記事がとてもわかり易いです. 管理リソースの開放,非管理リソースの開放はそれぞれどこに書くのか,どうしてそこに書くのか,についてとても丁寧に書かれています.

ufcpp.net

大事な点としては,

  • 非管理リソースは確実に開放するためにデストラクタからも開放されるようにする
  • 管理リソースの開放は.NET Frameworkに任せるので,デストラクタには開放処理を書かない

これらを満たすために上のようなDisposeパターンが使われる.

Disposeパターンを使った非管理リソースの開放については以下の記事にちょっと書きました.

shuhelohelo.hatenablog.com

C# 非管理リソースの開放 (一時ファイル作成後の後始末について)

リソースの開放はIDisposableインターフェースを実装して,開放処理を記述します.

これについては以下の記事がとてもわかり易いです.

ufcpp.net

さて,この記事では「非管理リソースの開放」について書きます.

例えば,Path.GetTempFileName()メソッドで一時ファイルを作成したとします.

このメソッドはサイズ0バイトとのファイルを(Windowsでは)C:\Users\{User name}\AppData\Local\Tempフォルダ内に生成します. この一時ファイルに保存したいデータを書き込んだりして使います.

この一時ファイルですが,アプリケーションの終了時に自動的に削除されるわけではなく,残り続けます. ですので,利用し終わったら適切に削除する必要があります.

この一時ファイルは.NET Frameworkの管理外のため,非管理リソースとなります. このような非管理リソースを開放するには,一例として以下のようにすることができます.

一時ファイルを生成するためのクラスを作成し,このクラスにIDisposableを実装して,非管理リソースである一時ファイルが削除されるようにしています.

    public class TemporaryFile : IDisposable
    {
        private string fullName = Path.GetTempFileName();
        private bool disposedValue = false;

        public string FullName
        {
            get => fullName;
        }

        public void Dispose()
        {
            //GC前にプログラム的にリソースを破棄するので
            //管理,非管理リソース両方が破棄されるようにする
            Dispose(true);
            GC.SuppressFinalize(this);//破棄処理は完了しているのでGC不要の合図
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposedValue)
            {
                return;
            }

            if (disposing)
            {
                //管理リソースの破棄処理
            }

            //非管理リソースの破棄処理
            try
            {
                File.Delete(this.fullName);
            }
            catch
            {
                throw;
            }

            disposedValue = true;
        }

        ~TemporaryFile()
        {
            //GC時に実行されるデストラクタでは非管理リソースの削除のみ
            Dispose(false);
        }
    }

ここで使用しているのが,Disposeパターンです.

shuhelohelo.hatenablog.com

使い方

   using (var tempFile = new TemporaryFile())
    {
        File.WriteAllText(tempFile.FullName,"hello");
        //....
    }

using句を抜けるときにtempFileオブジェクトがDisposeされ,その際に一時ファイルがDeleteされます.

iTextSharpでPDFを回転,選択する

使用するライブラリ

公式ではありません. NuGetからインストールします.

PDFファイルの読み込み

まずはPDFファイルを読み込みます.

PdfReader reader = PdfReader(filePath);

PDF情報の取得

このPdfReaderオブジェクトからはいろいろな情報を取得できます.

例えば以下のコードを実行してみましょう.

            //PDFファイルの読み込み
            PdfReader reader = new PdfReader(inputFilePath);

            //PDFファイルの各種情報の取得
            Console.WriteLine("Page count: {0}", reader.NumberOfPages);
            Console.WriteLine("Page rotation: {0}", reader.GetPageRotation(1));
            Console.WriteLine("Page size: {0}", reader.GetPageSize(1));
            Console.WriteLine("Page size with rotation: {0}", reader.GetPageSizeWithRotation(1));

指定したPDFに関する情報が出力されます. 今回指定したPDFは1ページ目を右に90°回転させてあり,用紙サイズはLetterサイズ(612*792)のものです.

Page count: 5
Page rotation: 90
Page size: Rectangle: 612x792 (rot: 0 degrees)
Page size with rotation: Rectangle: 792x612 (rot: 90 degrees)

指定したページの情報が取得できています.

ページ数の取得

int pageCount = reader.NumberOfPages;

指定したページの取得

GetPageNメソッドを使います. int型でページ番号を指定すると,そのページのオブジェクトを取得できます.

PdfDictionary page = _pdfReader.GetPageN(pageNumber);

ページの情報の取得

PdfDictionaryオブジェクトのGetAsNumberメソッドを使います.

PdfDictionaryという名前のとおり,引数にKeyを指定すると,そのKeyに対応するオブジェクトを取得できるようです.

例えば,ページの向きの情報を取得する場合は以下のようにします.

PdfNumber rotation = page.GetAsNumber(PdfName.Rotate);

角度をint型で取得する場合はIntValueプロパティで取得できますが,一つ注意が必要です. そのページが回転されていない,つまり0°の場合はpage.GetAsNumber(PdfName.Rotate)nullを返します.なので,nullチェックを挟む必要があります.

                PdfDictionary page = reader.GetPageN(2);
                //角度
                PdfNumber rotation = page.GetAsNumber(PdfName.Rotate);
                Console.WriteLine("Page rotation: {0}", rotation == null ? 0 : rotation.IntValue);

ページの回転

ページを回転させるには,そのページの角度を表すオブジェクトに新しい角度を表すオブジェクトをセットします.

まず,新しい角度を表すオブジェクトを作成します.

元の向きを基準に指定した角度回転させる,ということなので,以下のように「元の角度に回転させる角度を足して,それを360で割った余り」を計算して引数に与えます.

PdfNumber newRotation = new PdfNumber((元の角度+回転角) % 360);

次に,そのオブジェクトをPdfDictionaryオブジェクトにセットします.これにはPutメソッドを使用します.

Rotateに対してnewRotationをセットします.

page.Put(PdfName.Rotate, newRotation);

上記の2ステップでページを回転させることができます.

例えば,指定したページを右に90°回転させたい場合は以下のようにします.

            //ページの回転(右に90°回転)
            PdfNumber rotation = page.GetAsNumber(PdfName.Rotate);
            PdfNumber newRotation = new PdfNumber(rotation == null ? 90 : (rotation.IntValue + 90) % 360);
            page.Put(PdfName.Rotate, newRotation);

回転前後でページの角度を表示させてみます.

                //改めて角度を表示
                page = reader.GetPageN(1);
                rotation = page.GetAsNumber(PdfName.Rotate);
                Console.WriteLine("Page new rotation: {0}", rotation == null ? 0 : rotation.IntValue);

以下のように,90°回転していることが確認できます.

Page rotation: 90
Page new rotation: 180

編集後のファイル保存

保存は以下のようにPdfStamperクラスを使って別のファイルに書き込みます.

                //別ファイルに保存
                string outputFilePath = inputFilePath + "_output.pdf";//適当
                using (FileStream fs = new FileStream(outputFilePath, FileMode.OpenOrCreate))
                {
                    PdfStamper stamper = new PdfStamper(reader, fs);
                    stamper.Close();
                }

一つ注意が必要ですが,ここで読み込んだファイルに上書きするつもりで以下のようにすると,

                //同じファイルに保存(失敗する)
                using (FileStream fs = new FileStream(inputFilePath, FileMode.OpenOrCreate))
                {
                    PdfStamper stamper = new PdfStamper(reader, fs);
                    stamper.Close();
                }

すでに開かれているという理由でエラーになります.

f:id:shuhelohelo:20201022104743p:plain

今の所,スマートな保存の方法がわからないので,暫定的に以下のように,ファイルを開くときに一時ファイルにコピーしてから開くことで,保存時の上記エラーを回避しています.

            //一時ファイル経由のPDF読み込み
            string tempFilePath = Path.GetTempFileName();
            File.Copy(inputFilePath, tempFilePath, true);
            PdfReader reader = new PdfReader(tempFilePath);

ページの選択

PdfReaderのSelectPagesメソッドを使います.

以下のように選択したいページ番号をカンマ区切りで指定することで,読み込んだPDFファイルが指定したページのみになります.

                //1,3ページだけ選択
                reader.SelectPages("1,3");

また,ページの指定の仕方は以下のように-を使って範囲で指定することもできます.

                //1,2,3,5ページだけ選択
                reader.SelectPages("1-3,5");

ソースコード

https://github.com/shuheydev/iTextSharpLGPLv2CorePractice

Flutter開発環境のアップグレード

Flutterの開発環境に新しいバージョンが出たらアップグレードしたいですよね.

こちらの記事でも書きましたが,Flutterの開発環境構築はとても簡単に行えます.

shuhelohelo.hatenablog.com

この記事では上記記事の方法でFlutter開発環境を整えたものとして,開発環境のアップグレードの手順を書きます.

flutter doctorコマンドを実行

開発環境構築時にもお世話になったflutter doctorコマンドを実行します.

> flutter doctor

Flutterに新しいバージョンがある場合,以下のようなメッセージが表示されます.

f:id:shuhelohelo:20201016105613p:plain

flutter upgradeコマンドを実行

指示どおりにflutter upgradeコマンドを実行すると,以下のようにアップグレードが始まります.

f:id:shuhelohelo:20201016110745p:plain

アップグレードが完了し以下のバージョンに更新されました.

f:id:shuhelohelo:20201016110950p:plain

Xamarin.FormsのShellの項目の見た目を変更する

Shellを使うと最近のモバイルアプリケーションでよく使われている,ドロワーメニューというのでしょうか,画面端からにゅっと出てくるメニューを簡単に用意できます.

f:id:shuhelohelo:20200930121012p:plain

Xamarin.FormsではこれをFlyoutと呼びますが,これについて詳しくは以下の公式ドキュメントを御覧ください.

docs.microsoft.com

さて,これから書く内容はもちろん上記の公式ドキュメントにも書いてあることですが,自分なりのメモとして自分でも書きます.

このFlyoutに表示される項目は,例えば以下のようにFlyoutItem要素で記述します.

    <FlyoutItem Title="About" Icon="icon_about.png">
        <ShellContent Route="AboutPage" ContentTemplate="{DataTemplate local:AboutPage}" />
    </FlyoutItem>
    <FlyoutItem Title="Browse" Icon="icon_feed.png">
        <ShellContent Route="ItemsPage" ContentTemplate="{DataTemplate local:ItemsPage}" />
    </FlyoutItem>

この記述が冒頭のスクリーンショットのように,

[アイコン] [タイトル]

の並びで表示されます.

さて,この各項目の見た目を変更したいときがあります.

その場合は,項目の見た目を定義するテンプレートを用意します.

それがShell.ItemTemplateです.以下のようにDataTemplateの中にどのように表示させるかを記述するだけですが,FlyoutItem要素のTitleIconプロパティをバインディングして使うことができます. (IconFlyoutIconと指定しても良い)

    <Shell.ItemTemplate>
        <DataTemplate>
            <StackLayout Orientation="Horizontal">
                <Label Text="{Binding Title}"/>
                <Image Source="{Binding Icon}"/>
            </StackLayout>
        </DataTemplate>
    </Shell.ItemTemplate>

StackLayoutで横並びにタイトル,アイコンを表示するだけの雑な指定ですが,これをShell要素の中に配置すると,Flyoutの表示は以下のようになります.

f:id:shuhelohelo:20200930122357p:plain

ひどいですね.

とはいえ,このようにFlyoutItemの表示を自分の好きなように変更できます.

Azure Client SDKでリソースを作成する準備

docs.microsoft.com

Azureのリソースを作成する選択肢は,Azure Portal, Azure CLI, Azure PowerShell, Azure REST API, Azure Client SDKなど様々あり, 目的や状況に適したものを使うことができます.

ここではAzure Client SDKを使って,C#プログラムからAzureのリソースを作成するまでの手順をメモします.

具体的にはこちらです.

docs.microsoft.com

前提として,Azure CLIをインストールしておくこと.

SDKからAzureサブスクリプション内のリソースの読み取り,作成を行うアクセス許可を.NETアプリケーションに与える必要がある.

サービスプリンシパルを作成して,その資格情報でアクセスを許可するようにアプリを構成する.

  1. Azure Cloud Shellにログインする
  2. az account showを実行.以下の情報がJSON形式で得られる.
{
  "environmentName": "AzureCloud",
  "id": "15dbcfa8-4b93-4c9a-881c-6189d39f04d4",
  "isDefault": true,
  "name": "my-subscription",
  "state": "Enabled",
  "tenantId": "43413cc1-5886-4711-9804-8cfea3d1c3ee",
  "user": {
    "cloudShellID": true,
    "name": "jane@contoso.com",
    "type": "user"
  }
}
  1. az ad sp create-for-rbac --sdk-authを実行してサービスプリンシパルを作成する.

サービスプリンシパルとは以下のとおり。

サービス プリンシパル - 特定の Azure リソースにアクセスするためにアプリケーションまたはサービスによって使用されるセキュリティ ID です。 アプリケーションに対する "ユーザー ID" (ユーザー名とパスワード、または証明書) と考えることができます。

これは,Azure Active Directoryの範疇のようだ.

すると,以下のサービスプリンシパル情報がJSON形式で得られる.

{
  "clientId": "b52dd125-9272-4b21-9862-0be667bdf6dc",
  "clientSecret": "ebc6e170-72b2-4b6f-9de2-99410964d2d0",
  "subscriptionId": "ffa52f27-be12-4cad-b1ea-c2c241b6cceb",
  "tenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
  "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
  "resourceManagerEndpointUrl": "https://management.azure.com/",
  "activeDirectoryGraphResourceId": "https://graph.windows.net/",
  "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
  "galleryEndpointUrl": "https://gallery.azure.com/",
  "managementEndpointUrl": "https://management.core.windows.net/"
}

これをアプリケーションから利用する際は,userSecrets.json,に入れておき,安全を確保すると良い.

{
  "azure": {
    "credentials": {
      "clientId": "b52dd125-9272-4b21-9862-0be667bdf6dc",
      "clientSecret": "ebc6e170-72b2-4b6f-9de2-99410964d2d0",
      "subscriptionId": "ffa52f27-be12-4cad-b1ea-c2c241b6cceb",
      "tenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
      "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
      "resourceManagerEndpointUrl": "https://management.azure.com/",
      "activeDirectoryGraphResourceId": "https://graph.windows.net/",
      "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
      "galleryEndpointUrl": "https://gallery.azure.com/",
      "managementEndpointUrl": "https://management.core.windows.net/"
    }
  }
}

利用する際は以下のように.(参考)

using System;
using System.IO;
using Microsoft.Extensions.Configuration;

namespace AzureSDKPractice
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            var confBuilder = new ConfigurationBuilder();
            var conf = confBuilder
                .AddUserSecrets<Program>(true)
                .Build();

            var clientId = conf["azure:credentials:clientId"];
            var clientSecret = conf["azure:credentials:clientSecret"];
            var tenantId = conf["azure:credentials:tenantId"];
            var subscriptionId = conf["azure:credentials:subscriptionId"];

....略

Virtual Networkの作成

サンプルにあるように作成する.

gist.github.com

            var creatableNetwork = azure.Networks
                            .Define("az-fluent-vnet")
                            .WithRegion(Region.UK_WEST)
                            .WithExistingResourceGroup("testing")
                            .WithAddressSpace("172.16.0.0/16");

OS Imageの確認の仕方

VMを作成する際には使用するOSのイメージを指定する必要がある. 指定に必要な情報はpublisher,"offer","sku"の3つの情報. これらはaz vm image listで確認でき,以下のJSON形式の情報が得られる.

[
  {
    "offer": "CentOS",
    "publisher": "OpenLogic",
    "sku": "7.5",
    "urn": "OpenLogic:CentOS:7.5:latest",
    "urnAlias": "CentOS",
    "version": "latest"
  },
  {
    "offer": "CoreOS",
    "publisher": "CoreOS",
    "sku": "Stable",
    "urn": "CoreOS:CoreOS:Stable:latest",
    "urnAlias": "CoreOS",
    "version": "latest"
  },
  {
    "offer": "debian-10",
    "publisher": "Debian",
    "sku": "10",
    "urn": "Debian:debian-10:10:latest",
    "urnAlias": "Debian",
    "version": "latest"
  },
  {
    "offer": "openSUSE-Leap",
    "publisher": "SUSE",
    "sku": "42.3",
    "urn": "SUSE:openSUSE-Leap:42.3:latest",
    "urnAlias": "openSUSE-Leap",
    "version": "latest"
  },
  {
    "offer": "RHEL",
    "publisher": "RedHat",
    "sku": "7-LVM",
    "urn": "RedHat:RHEL:7-LVM:latest",
    "urnAlias": "RHEL",
    "version": "latest"
  },
  {
    "offer": "SLES",
    "publisher": "SUSE",
    "sku": "15",
    "urn": "SUSE:SLES:15:latest",
    "urnAlias": "SLES",
    "version": "latest"
  },
  {
    "offer": "UbuntuServer",
    "publisher": "Canonical",
    "sku": "18.04-LTS",
    "urn": "Canonical:UbuntuServer:18.04-LTS:latest",
    "urnAlias": "UbuntuLTS",
    "version": "latest"
  },
  {
    "offer": "WindowsServer",
    "publisher": "MicrosoftWindowsServer",
    "sku": "2019-Datacenter",
    "urn": "MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest",
    "urnAlias": "Win2019Datacenter",
    "version": "latest"
  },
  {
    "offer": "WindowsServer",
    "publisher": "MicrosoftWindowsServer",
    "sku": "2016-Datacenter",
    "urn": "MicrosoftWindowsServer:WindowsServer:2016-Datacenter:latest",
    "urnAlias": "Win2016Datacenter",
    "version": "latest"
  },
  {
    "offer": "WindowsServer",
    "publisher": "MicrosoftWindowsServer",
    "sku": "2012-R2-Datacenter",
    "urn": "MicrosoftWindowsServer:WindowsServer:2012-R2-Datacenter:latest",
    "urnAlias": "Win2012R2Datacenter",
    "version": "latest"
  },
  {
    "offer": "WindowsServer",
    "publisher": "MicrosoftWindowsServer",
    "sku": "2012-Datacenter",
    "urn": "MicrosoftWindowsServer:WindowsServer:2012-Datacenter:latest",
    "urnAlias": "Win2012Datacenter",
    "version": "latest"
  },
  {
    "offer": "WindowsServer",
    "publisher": "MicrosoftWindowsServer",
    "sku": "2008-R2-SP1",
    "urn": "MicrosoftWindowsServer:WindowsServer:2008-R2-SP1:latest",
    "urnAlias": "Win2008R2SP1",
    "version": "latest"
  }
]

最終的に以下のコードで仮想マシンが作成される.

        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            var confBuilder = new ConfigurationBuilder();
            var conf = confBuilder
                .AddUserSecrets<Program>(true)
                .Build();

            var clientId = conf["azure:credentials:clientId"];
            var clientSecret = conf["azure:credentials:clientSecret"];
            var tenantId = conf["azure:credentials:tenantId"];
            var subscriptionId = conf["azure:credentials:subscriptionId"];

            var credentials = SdkContext.AzureCredentialsFactory
                .FromServicePrincipal(clientId, clientSecret, tenantId, AzureEnvironment.AzureGlobalCloud);
            var azure = Azure.Configure()
                .Authenticate(credentials)
                .WithSubscription(subscriptionId);//明示するのを推奨.


            var creatableNetwork = azure.Networks
                .Define("test-vm")
                .WithRegion(Region.JapanEast)
                .WithExistingResourceGroup("TestResourceGroup")
                .WithAddressSpace("172.16.0.0/16");

            var vmName = "test-vm";
            var rootId = conf["azure:vmRootInfo:rootId"];
            var rootPass = conf["azure:vmRootInfo:rootPassword"];

            azure.VirtualMachines
                .Define(vmName)
                .WithRegion(Region.JapanEast)
                .WithExistingResourceGroup("TestResourceGroup")
                .WithNewPrimaryNetwork(creatableNetwork)
                .WithPrimaryPrivateIPAddressDynamic()
                .WithoutPrimaryPublicIPAddress()
                .WithLatestLinuxImage("Canonical", "UbuntuServer", "18.04-LTS")
                .WithRootUsername(rootId)
                .WithRootPassword(rootPass)
                .WithComputerName(vmName)
                .WithSize(VirtualMachineSizeTypes.BasicA0)
                .Create();
        }

f:id:shuhelohelo:20200906171000p:plain

なるほど.

ソースコード

https://github.com/shuheydev/AzureSDKPractice