20191125のC#に関する記事は10件です。

Blazorで未ログイン時にログインページにリダイレクトする

概要

Blazorで未ログイン時にログインページにリダイレクトする方法のメモ。
試しているのはClident-side版ですが、恐らくServer-side版でも動くと思われます。

背景等

Blazorではログイン時、未ログイン時に表示するページの認証制御機能などが標準機能として提供されています。(参照)

これらの機能を組み合わせて、認証時にのみアクセス可能なページを作成可能です。
良く紹介されている例として、認証が必要なページに未認証な状態でアクセスした場合に、下記のような未認証状態と表示させる例があります。

App.razor
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
  <AuthorizeRouteView RouteData="@routeData" 
            DefaultLayout="@typeof(MainLayout)">
    <NotAuthorized>
     <h1>未ログインのため表示できません。</h1>
    </NotAuthorized> 
 </AuthorizeRouteView >
</Found>
</Router>
Counter.razor
@page "/counter"
@attribute [Authorize]
@inherits CounterModel

<h1>Counter</h1>

<p>Current count: @CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

App.razor内でAuthorizeRouteViewを定義し、未認証状態の場合にはNotAuthorized属性の内容を表示するようにします。

各ページのコンポーネントにAuthorize属性を付与することで、そのページは認証状態でしかアクセス不能になり、未認証時には上記のNotAuthorized属性の内容が表示されます。

しかしながら実際のウェブサイトでは、アクセス不可のページを表示するのではなく、ログインページにリダイレクトすることが多いと思います。
今回はその実現方法に関する紹介です。

実装方法

コンポーネントの作成

ログインページにリダイレクトするrazorコンポーネントを作成します。

RidirectToLogin.razor
@inject NavigationManager Navigation
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo("/Login", false);
    }
}

このコンポーネントはUI要素は持たず、コンポーネントの初期化処理(OnInitialized)でログインページへの遷移を行うだけの機能となります。

コンポーネントの配置

作成したRedirectToLoginコンポーネントをApp.razorに配置します。

App.razor
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
            <NotAuthorized>
                <RedirectToLogin />
            </NotAuthorized>
        </AuthorizeRouteView>
    </Found>
</Router>

これで、未認証時にはRidirectToLoginコンポーネントが実行され、ログインページにリダイレクトされます。

参考資料

https://docs.microsoft.com/ja-jp/aspnet/core/security/blazor/?view=aspnetcore-3.0&tabs=visual-studio
https://github.com/aspnet/AspNetCore/issues/13709
https://visualstudiomagazine.com/articles/2019/10/25/authorizing-users-in-blazor.aspx

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

自動でBootシーンに切り替える仕組みを作ると人生が楽になる 【Unity】

ゲームを作る時に、初期化用のシーンを経由してから指定したシーンに移行し動作させることってあると思うのですが
そういう設計にすると制作中テストする度に初期化用のシーンに戻る必要が出てきて少し大変ですよね

この問題はUnityのEditor拡張を利用することで解決できます。

実装は簡単。再生時にシーンを切り替える命令を登録するだけ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.SceneManagement;

[InitializeOnLoad]
public static class PlayModeEditor
{
    static string m_bootSceneName ="Boot";// ←移動したいシーンの名前を記入する

    static PlayModeEditor()
    {
        EditorApplication.playModeStateChanged += ChangeBootScene;
    }

    static void ChangeBootScene(PlayModeStateChange state)
    {
        // 実行状態になったら
        if (state == PlayModeStateChange.EnteredPlayMode)
        {
            // 別シーンで起動していた場合切り替える
            Scene scene = SceneManager.GetActiveScene();
            if (!scene.Equals(m_bootSceneName))
            {
                SceneManager.LoadScene(m_bootSceneName);
            }
        }
    }    
}

実行時の結果

playmode.gif

この機能を利用すれば他でも応用できそうです。

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

ComboBoxで、PreviewKeyDown時の矢印キーを認識したい

could not capture down arrow in combobox in wpf
に同じ課題があったが、解決しなかったので、次の方法で解決した旨を記載。

ComboBoxをStackPanelの中に配置し、StackPanelのPreviewKeyDownイベントで認識する。

ComboBoxの、PreviewKeyUpでは認識できるのに、Downではできなかったため。

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

ジョレボーヌーボー

本家コメント欄にいろいろあったの気付かず立てちゃいました。ゴメンナサイ


C# 版。LINQ でワンライナー。

using System;
using System.Linq;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(string.Concat(new[] { "ボ", "ジョ", "レ", "ヌ", "ボ" }
                .OrderBy(_ => Guid.NewGuid())
                .Select((x, i) => i > 1 ? x + "ー" : x)));
        }
    }
}

元ネタ

ヌジョレーボーボー

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

イベントハンドラまで対応したコントロール配列を作る

只今絶賛VB6からVB.NET (VS2017)への移行作業中です。
移行作業で非互換となる機能の一つにコントロール配列があります。
Microsoft.VisualBasic.Compatibility.VB6互換ライブラリを使用すればいいのですが
Deprecatedというわけで使用禁止。

止む無くTextBoxのコントロール配列クラスを自作することとなりました。

目標は

public WithEvents Text1 As Microsoft.VisualBasic.Compatibility.VB6.TextBoxArray

public WithEvents Text1 As MyLib.TextBoxArray

のように置き換えるだけで済むクラスを作りたいというものです。
ネット検索では次のようなイベントハンドラにまで対応した例を見つけることが出来ませんでしたのでこれに対応するのが第一となります。

コントロール配列に対するイベントハンドラの例
'Text1はコントロール配列
Private Sub Text1_KeyPress(sender As Object, e As KeyPressEventArgs) Handles Text1.KeyPress
    Console.WriteLine("KeyPress")
End Sub

クラスの定義は

Microsoft.VisualBasic.Compatibility.VB6.TextBoxArrayのドキュメント
https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.visualbasic.compatibility.vb6.textboxarray?view=netframework-4.8
および
Microsoft.VisualBasic.Compatibility.VB6.BaseControllArrayのドキュメント
https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.visualbasic.compatibility.vb6.basecontrolarray.hookupcontrolevents?view=netframework-4.8#Microsoft_VisualBasic_Compatibility_VB6_BaseControlArray_HookUpControlEvents_System_Object_

に準拠させるとして

Designer.vbファイルの中身

'中略
public WithEvents Text1 As Microsoft.VisualBasic.Compatibility.TextBoxArray
'中略
Me.Text1 = New  Microsoft.VisualBasic.Compatibility.TextBoxArray(Me.components)
'中略
CType(Me.Text1, System.ComponentModel.ISupportInitialize).BeginInit()
'中略

Me.Text1.SetIndex(Me._Text1_3, CType(3, Short))
Me.Text1.SetIndex(Me._Text1_2, CType(2, Short))
Me.Text1.SetIndex(Me._Text1_1, CType(1, Short))
Me.Text1.SetIndex(Me._Text1_0, CType(0, Short))
'中略
CType(Me.Text1, System.ComponentModel.ISupportInitialize).EndInit()
'中略

を参考に必要最小限に絞って作成した互換クラスが以下の通り
(ここに至るまで紆余曲折ありましたが全部省略)

BaseControlArray.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;

namespace MyLib
{
    public abstract class BaseControlArray : Component, ISupportInitialize
    {
        // コントロール配列で管理するクラスをDictionaryに入れる
        // (Listとかに使用と思ったらインデックス-1で登録されてくるケースがあったので
        // Dictionaryにしました。)
        protected Dictionary<int, Control> items = new Dictionary<int, Control>();
        //  コントロール配列内のコントロール数を返します。
        public short Count()
        {
            return checked((short)this.items.Count);
        }

        // 何もしないがこれがないとコンパイルエラー
        public void BeginInit()
        {

        }

        // 何もしないがこれがないとコンパイルエラー
        public void EndInit()
        {

        }

        // 気持ちの問題か
        protected override void Dispose(bool disposing) 
        {
            if(disposing)
            {
                items.Clear();
            }
            base.Dispose(disposing);
        }
        // 必要かどうか迷ったが入れておく
        public short LBound()
        {
            return 0;
        }

        // 必要かどうか迷ったが入れておく
        public short UBound()
        {
            return (short)(items.Count - 1);
        }

        public IEnumerator GetEnumerator()
        {
            return items.GetEnumerator();
        }      
    }
}
TextBoxArray.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;

namespace MyLib
{
    [ProvideProperty("Index", typeof(TextBox))]
    public class TextBoxArray : BaseControlArray, IExtenderProvider
    {
        // コンストラクタの中で何かする必要はなかった
        public TextBoxArray()
        {
        }
        // コンストラクタの中で何かする必要はなかった
        public TextBoxArray(IContainer Container)
        {
        }


        // KeyPressイベントハンドラ
        public event KeyPressEventHandler KeyPress;

        // 与えられたこのコントロール配列に入れることが出来るかどうかを返す
        public bool CanExtend(object extendee)
        {
            return extendee is TextBox;
        }
        // コントロール配列の型を返す
        protected Type GetControlInstanceType()
        {
            return typeof(TextBox);
        }

        // コントロール配列にコントロールを登録
        public void SetIndex(TextBox item, short index)
        {
            items[index] = item;
            // コントロール配列のイベントを個々のコントロールに結び付ける
            if (KeyPress != null) item.KeyPress += KeyPress;
        }

        public short GetIndex(TextBox item)
        {
            IEnumerator<KeyValuePair<int, Control>> enumerator = items.GetEnumerator();
            while (enumerator.MoveNext())
            {
                if (enumerator.Current.Value == item) return (short)enumerator.Current.Key;
            }
            return (short)0;
        }

        public TextBox this[short Index]
        {
            get
            {
                return (TextBox)items[Index];
            }
        }

    }
}

イベントハンドラはお好みで追加してください。
ほかの種類のコントロール配列はTextBoxArrayのコピペで行けると思います。

以上

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

UnityTestRunner向けTest属性を自作してみる

前書き

アドカレ4日目です。
UnityTestRunnerについてちょっと書いてみます。

テスト、いいですよね。たくさんテストを書くと仕事が進んでいるような気がしてきます。
僕はそんなにテスト書かない人間ですけど、テストを書くのが嫌いでは無いです。
最近、UnityTestRunnerとその裏側であるNUnitに触れていたら、TestAttributeの仕組みが気になって、自作のAttributeとかを噛ませてみたくなったので、試してみました。

シンプルな自作TestAttribute

まずはじめに [SimpleTest] public void テストメソッド() { } こんな感じでTestRunnerに認識されるAttributeを自作してみます。

実装と使用方法

TestAttribute定義

SimpleTestAttribute.cs
using System;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class SimpleTestAttribute : NUnitAttribute, ISimpleTestBuilder, IImplyFixture
{
    public TestMethod BuildFrom(IMethodInfo method, Test suite)
        => new TestMethod(method, suite);
}

使用方法

SimpleTestSample.cs
using NUnit.Framework;

public class SimpleTestSample
{
    [SimpleTest] public void Test() { }
    [SimpleTest] public void TestFail() => Assert.Fail();
}

上記のテストケースであれば、UnityTestRunner上では次のスクリーンショットのように表示されます。
スクリーンショット 2019-11-25 午後4.49.33.png

仕組み

UnityTestRunnerに認識させるためにやったことはAttributeの定義(とテストメソッドへの付与)だけです。

継承元について

NUnit.Framework.NUnitAttributeを継承していますが、これは System.Attribute を直接継承してもUnityTestRunner上では動作に違いはありませんでしたが、その場の雰囲気でNUnitAttributeを採用しました。
他にもNUnit標準のAttributeはいくつかあるのでNUnit内のAttributeを眺めるのもいいかもしれません。
例えば、UnityTestAttributeなんかはCombiningStrategyAttributeを継承していたりします。

ISimpleTestBuilderについて

NUnit.Framework.Interfaces.ISimpleTestBuilder は次のように定義されています。

ISimpleTestBuilder.cs
public interface ISimpleTestBuilder
{
  TestMethod BuildFrom(IMethodInfo method, Test suite);
}

今回はTestMethodのコンストラクタ TestMethod(IMethodInfo method, Test suite) にそのまま流し込んで完了としました。
もう少しイロイロしてくれる便利クラスとして NUnit.Framework.Internal.Builders.NUnitTestCaseBuilder というものも存在していて、標準の TestAttribute などはこのビルダーを使っているようです。

IImplyFixtureについて

NUnit.Framework.Interfaces.IImplyFixture 自体は何のメソッドも持たないマーカーインターフェースですが、最低限これだけ付いていればテストとして認識させることができます。ただし前述の ISimpleTestBuilder.BuildFrom のようなTestMethodを提供するインターフェースが存在しないと、何のテストも実行されません。


シーンを指定してTestを実行する属性の実装(その1)

テストメソッドを実行する直前に任意の処理を挟み込むAttributeを実装してみます。
今回の例ではテスト実行前に EditorSceneManager.OpenScene("hoge.unity"); を叩いてみます。

実装と使用方法

TestAttribute定義

UnityTestSceneAttribute.cs
using System;
using System.Reflection;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using UnityEditor.SceneManagement;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class UnityTestSceneAttribute : NUnitAttribute, ISimpleTestBuilder, IImplyFixture
{
    public readonly string scenePath;
    public UnityTestSceneAttribute(string scenePath) => this.scenePath = scenePath;

    public TestMethod BuildFrom(IMethodInfo method, Test suite)
        => new TestMethod(new MethodProxy(method, () => EditorSceneManager.OpenScene(scenePath)), suite);
}

// NUnit.Framework.Internal.MethodWrapperをさらに包むもの
class MethodProxy : IMethodInfo
{
    private readonly IMethodInfo _methodInfo;
    private readonly Action _beforeTest;

    public MethodProxy(IMethodInfo methodInfo, Action beforeTest)
    {
        _methodInfo = methodInfo;
        _beforeTest = beforeTest;
    }

    public object Invoke(object fixture, params object[] args)
    {
        _beforeTest.Invoke();
        return _methodInfo.Invoke(fixture, args);
    }

    public ITypeInfo TypeInfo => _methodInfo.TypeInfo;
    public MethodInfo MethodInfo => _methodInfo.MethodInfo;
    public string Name => _methodInfo.Name;
    public bool IsAbstract => _methodInfo.IsAbstract;
    public bool IsPublic => _methodInfo.IsPublic;
    public bool ContainsGenericParameters => _methodInfo.ContainsGenericParameters;
    public bool IsGenericMethod => _methodInfo.IsGenericMethod;
    public bool IsGenericMethodDefinition => _methodInfo.IsGenericMethodDefinition;
    public ITypeInfo ReturnType => _methodInfo.ReturnType;
    public T[] GetCustomAttributes<T>(bool inherit) where T : class => _methodInfo.GetCustomAttributes<T>(inherit);
    public bool IsDefined<T>(bool inherit) => _methodInfo.IsDefined<T>(inherit);
    public IParameterInfo[] GetParameters() => _methodInfo.GetParameters();
    public Type[] GetGenericArguments() => _methodInfo.GetGenericArguments();
    public IMethodInfo MakeGenericMethod(params Type[] typeArguments) => _methodInfo.MakeGenericMethod();
}

使用方法

using NUnit.Framework;
using UnityEngine;

public class UnityTestSceneTest
{
    [UnityTestScene("Assets/Scenes/CameraAru.unity")]
    public void AruScene() => Assert.IsNotNull(Object.FindObjectOfType<Camera>());

    [UnityTestScene("Assets/Scenes/CameraNai.unity")]
    public void NaiScene() => Assert.IsNull(Object.FindObjectOfType<Camera>());
}

次のスクリーンショットは、上記のテストコードをUnityTestRunnerで実行実行した結果です。
スクリーンショット 2019-11-25 午後4.56.49.png

仕組み

NUnitのIMethodInfoを実装してMethodProxyクラスを定義し、BuildFromに渡ってくるIMethodInfoのプロキシとしてInvokeに割り込んでいます。
なんとなくActionを渡していますが、直接Invokeメソッド内に書いちゃっても問題ないですね。

なお、今回のBuildFromメソッド実装では、色々実装が足りないのでValuesやRange属性と組み合わせることができませんが、そこはご愛嬌ということで。


シーンを指定してTestを実行する属性の実装(その2)

前述した「シーンを指定してTestを実行する属性の実装(その1)」の場合、色々な属性と組み合わせるとボロが出始めました。
そこで、もう少しボロが出にくいUnityTestRunnerライクな実装をしてみます。
使い方は一緒なので割愛します。

実装

UnityTestSceneAttribute.cs
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using UnityEditor.SceneManagement;
using UnityEngine.TestTools;

[System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = true)]
public class UnityTestSceneAttribute : NUnitAttribute, ISimpleTestBuilder, IImplyFixture, IOuterUnityTestAction
{
    public readonly string scenePath;
    public UnityTestSceneAttribute(string scenePath) => this.scenePath = scenePath;
    public TestMethod BuildFrom(IMethodInfo method, Test suite) => new TestMethod(method, suite);

    public System.Collections.IEnumerator BeforeTest(ITest test)
    {
        EditorSceneManager.OpenScene(scenePath);
        // 1frameくらい間を置いた方が安心
        yield return null;
    }

    public System.Collections.IEnumerator AfterTest(ITest test)
    {
        yield break;
    }
}

仕組み

都合よくUnityTestRunnerのテストの前後に処理を挟み込める IOuterUnityTestAction というインターフェースが用意されているので、これを自作Attributeに実装してあげるだけです。
テスト属性に密接に関わる処理は UnitySetUpUnityTearDown よりこちらのインターフェースを使った方が簡単なケースもあるかもしれません。


TestCaseSourceのUnityTest版

NUnitには TestCaseSourceAttribute というものがいて、このAttributeにイテレータを返すメンバー名を渡してあげると1つのテストから複数のテストケースを量産することができます。
TestAttribute に対するUnityTestRunnerの UnityTestAttribute のように、 TestCaseSourceAttribute に対して UnityTestCaseAttribute があるかなーっと思ったのですが、現状見つからなかったので自作してみました。
以下に実装を残します。

実装と使用方法

TestAttribute定義

UnityTestCaseSourceAttribute.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using NUnit.Framework.Internal.Builders;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class UnityTestCaseSourceAttribute : TestCaseSourceAttribute, ITestBuilder
{
    private readonly NUnitTestCaseBuilder _builder = new NUnitTestCaseBuilder();
    // コンストラクタ
    public UnityTestCaseSourceAttribute(string sourceName) : base(sourceName) { }
    public UnityTestCaseSourceAttribute(Type sourceType, string sourceName) : base(sourceType, sourceName) { }
    public UnityTestCaseSourceAttribute(Type sourceType) : base(sourceType) { }
    public UnityTestCaseSourceAttribute(Type sourceType, string sourceName, object[] methodParams) : base(sourceType, sourceName, methodParams) { }

    // ITestBuilder
    public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite)
    {
        var cases = (IEnumerable<ITestCaseData>) typeof(TestCaseSourceAttribute).InvokeMember("GetTestCasesFor",
            BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod,
            null, this, new object[] {method});
        return cases.OfType<TestCaseParameters>().Select(p => BuildFromImpl(method, suite, p));
    }
    private TestMethod BuildFromImpl(IMethodInfo method, Test suite, TestCaseParameters caseParam)
    {
        caseParam.ExpectedResult = new object();
        caseParam.HasExpectedResult = true;
        var t = _builder.BuildTestMethod(method, suite, caseParam);
        if (t.parms != null) t.parms.HasExpectedResult = false;
        return t;
    }
}

使用法

SimpleTestEx.cs
using System.Collections;
using NUnit.Framework;
using UnityEngine.Networking;

public class SimpleTestEx
{
    public static object[][] urls =
    {
        new object[] {"https://google.co.jp", 200},
        new object[] {"https://yahoo.co.jp", 200},
    };

    [UnityTestCaseSource("urls")]
    public IEnumerator UnityTestExのテスト(string url, int responseCode)
    {
        using (var req = UnityWebRequest.Get(url))
        {
            var ope = req.SendWebRequest();
            while (ope.isDone == false) yield return null;
            Assert.AreEqual(responseCode, req.responseCode);
        }
    }
}

次のスクリーンショットは、上記のテストコードをUnityTestRunnerで実行実行した結果です。
UnityTestCaseSourceAttribute

仕組み

TestCaseSourceAttributeを継承して、ITestBuilderを実装し直してみました。
TestCaseSourceAttributeにはvirtualなメソッドなんて存在しないので、BuildFromメソッドをnewして隠蔽し、GetTestCasesForメソッドをリフレクションでこじ開けるという、気合いと根性に満ちた実装になっています。
TestCaseSourceAttributeを写経するのも選択肢としてはありですが、少々複雑だったので横着してみました。
ぱっと見はそこそこ素直な実装に見えるんじゃないでしょうか。


アスキーアートだって

結構なんでもできるので、

[AATest]
public void AA表示したい() { }

たったこれだけのテストから
スクリーンショット 2019-12-02 午後6.32.54.png

こんな感じでアスキーアートを出すことだって自由です。

それではみなさん楽しいテストライフを!

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

C#の標準シリアライズ・デシリアライズを簡単にまとめてみた

まとめてみた

設定とか、何かしらのオブジェクトをざっと保存して、さっと読み込みたいなぁなんて思った時に、シリアライザーという言葉が目に付きました。

でもなんか色々ある...。
というわけでちょっと、簡単に使える感じにまとめてみました。

個人的には ReactiveProperty がシリアライズできる、できないみたいなところが重要です。
でも、そうなると XmlSerializer しか使えない。
Slim とかもできないみたいですけど、ドキュメントを漁っても ReactivePropertyのシリアライズに関する記事がない...。

探し方が悪い?
そういった使い方をしないとか?

なんちゃってプログラマーの私にはわからない...。

標準のシリアライザー達

  • XmlSerializer

    • 結構汎用的というか、使い勝手がいいイメージのあるシリアライザー
    • ReactiveProperty<T> とかもシリアライズできる。(他はなんかできなかった…。シリアライズはカスタムできるらしいので、何かしらこねこねすればいけるんでしょうか?)
    • 適用にプロパティを持ったクラスがあるなら、この子に放り込めばいい感じにしてくれる。安パイ的な存在。
    • 空のコンストラクタが必須。
  • BinaryFormatter

    • バイナリで高速。(でも場合によりけりてきな文言が目に付く...)
    • ReactivePropertyはシリアライズできない。
    • クラス属性に [Serializeble] を宣言する。
    • 空のコンストラクタが必須。
  • DataContractSerializer

    • 他ではできないDictionaryとか、ほとんどのオブジェクトをシリアライズできる。シリアライザーとしては強力な機能を持っているらしいです。
    • ReactivePropertyはシリアライズできない。
    • クラス属性に [DataContract] を宣言する。
    • プロパティには [DataMember] を付けないとシリアライズされない。Orderとかでシリアライズ順序を指定できる。
    • 空のコンストラクタがいらない。(シリアライズ時にコンストラクタが呼び出されない)
  • DataContractJsonSerializer

    • オブジェクトをJSON形式でシリアライズ・デシリアライズできる。
    • DataContractSerializerよりも柔軟性がある。
    • 後は、XMLとの相互変換するような機能がある。(インテリセンスのオーバーロードに出たのを見ただけで試してないですが…)
    • JSON.Netとかライブラリがあるようなのでそちらを使った方がよい?
    • ReactivePropertyはシリアライズできない。
  • XamlWriter

    • ButtonとかのUI要素をシリアライズできる。
    • 画面レイアウトの保存とかに使える?
  • XamlReader

    • ButtonとかのUI要素をデシリアライズできる。
    • 画面レイアウトの復元とかに使える?

以下、コードです。

using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization.Json;
using System.Windows;
using System.Windows.Markup;
using System.Xml;
using System.Xml.Serialization;

namespace Serializers
{
    public static class XLizer
    {
        /// <summary>
        /// <see cref="XmlWriter"/> のネームスペース設定
        /// </summary>
        private static XmlSerializerNamespaces xmlWriterEmptyNameSpace { get { var empty = new XmlSerializerNamespaces(); empty.Add("", ""); return empty; } }

        /// <summary>
        /// <see cref="XmlWriter"/> のシリアライズ設定
        /// </summary>
        private static XmlWriterSettings xmlWriterSettings { get { return new XmlWriterSettings { Indent = true }; } }

        /// <summary>
        /// <see cref="XmlReader"/> のデシリアライズ設定
        /// </summary>
        private static XmlReaderSettings xmlReaderSettings { get { return new XmlReaderSettings { CheckCharacters = false }; } }

        /// <summary>
        /// <see cref="DataContractJsonSerializer"/> のシリアライズ設定
        /// </summary>
        //private static DataContractJsonSerializerSettings dataContractJsonSerializerSettings { get { return new DataContractJsonSerializerSettings { }; } }

        /// <summary>
        /// <para><see cref="XmlSerializer"/> を使用して <see cref="object"/> をシリアライズします。</para>
        /// </summary>
        public static bool XmlSerialize<T>(string filePath, T dataObject)
        {
            try {

                using XmlWriter xmlWriter =
                    XmlWriter.Create(
                        filePath,
                        xmlWriterSettings);

                XmlSerializer xmlSerializer =
                    new XmlSerializer(
                        typeof(T));

                xmlSerializer.Serialize(
                    xmlWriter,
                    dataObject,
                    xmlWriterEmptyNameSpace);

            } catch {
                return false;
            }
            return true;
        }

        /// <summary>
        /// <para><see cref="XmlSerializer"/> を使用して <see cref="object"/> をデシリアライズします。</para>
        /// </summary>
        public static bool XmlDeserialize<T>(string filePath, out T dataObject)
        {
            try {

                using XmlReader xmlReader =
                    XmlReader.Create(
                        filePath,
                        xmlReaderSettings);

                XmlSerializer xmlSerializer =
                    new XmlSerializer(
                        typeof(T));

                var obj =
                    xmlSerializer.Deserialize(
                        xmlReader);

                dataObject = (T)obj;

            } catch {
                dataObject = default;
                return false;
            }
            return true;
        }

        /// <summary>
        /// <para><see cref="BinaryFormatter"/> を使用して <see cref="object"/> をシリアライズします。</para>
        /// <code>
        ///     <para>[<see cref="Serializable"/>]</para>
        ///     <para>class MyClass { ... }</para>
        /// </code>
        /// </summary>
        public static bool BinarySerialize<T>(string filePath, T dataObject)
        {
            try {

                using FileStream fileStream =
                    new FileStream(
                        filePath,
                        FileMode.Create);

                BinaryFormatter binaryFormatter =
                    new BinaryFormatter();

                binaryFormatter.Serialize(
                    fileStream,
                    dataObject);

            } catch {
                return false;
            }
            return true;
        }

        /// <summary>
        /// <para><see cref="BinaryFormatter"/> を使用して <see cref="object"/> をデシリアライズします。</para>
        /// </summary>
        public static bool BinaryDeserialize<T>(string filePath, out T dataObject)
        {
            try {

                using FileStream fileStream =
                    new FileStream(
                        filePath,
                        FileMode.Open);

                BinaryFormatter binaryFormatter =
                    new BinaryFormatter();

                var obj =
                    binaryFormatter.Deserialize(
                        fileStream);

                dataObject = (T)obj;

            } catch {
                dataObject = default;
                return false;
            }
            return true;
        }

        /// <summary>
        /// <para><see cref="DataContractSerializer"/> を使用して <see cref="object"/> をシリアライズします。</para>
        /// <code>
        ///     <para>[<see cref="DataContract"/>]</para>
        ///     <para>class MyClass { ... }</para>
        /// </code>
        /// <code>
        ///     <para>[<see cref="DataMember"/>]</para>
        ///     <para>string Field { ... }</para>
        /// </code>
        /// </summary>
        public static bool DataContractSerialize<T>(string filePath, T dataObject)
        {
            try {

                using XmlWriter xmlWriter =
                    XmlWriter.Create(
                        filePath,
                        xmlWriterSettings);

                DataContractSerializer dataContractSerializer =
                    new DataContractSerializer(
                        typeof(T));

                dataContractSerializer.WriteObject(
                    xmlWriter,
                    dataObject);

            } catch {
                return false;
            }
            return true;
        }

        /// <summary>
        /// <para><see cref="DataContractSerializer"/> を使用して <see cref="object"/> をシリアライズします。</para>
        /// </summary>
        public static bool DataContractDeserialize<T>(string filePath, out T dataObject)
        {
            try {

                using XmlReader xmlReader =
                    XmlReader.Create(
                        filePath,
                        xmlReaderSettings);

                DataContractSerializer dataContractSerializer =
                    new DataContractSerializer(
                        typeof(T));

                var obj =
                    dataContractSerializer.ReadObject(
                        xmlReader);

                dataObject = (T)obj;

            } catch {
                dataObject = default;
                return false;
            }
            return true;
        }

        /// <summary>
        /// <para><see cref="DataContractJsonSerializer"/> を使用して <see cref="object"/> をシリアライズします。</para>
        /// <code>
        ///     <para>[<see cref="DataContract"/>]</para>
        ///     <para>class MyClass { ... }</para>
        /// </code>
        /// <code>
        ///     <para>[<see cref="DataMember"/>]</para>
        ///     <para>string Field { ... }</para>
        /// </code>
        /// </summary>
        public static bool DataContractJsonSerialize<T>(string filePath, T dataObject)
        {
            try {

                using FileStream fileStream =
                    new FileStream(
                        filePath,
                        FileMode.Create);

                DataContractJsonSerializer dataContractJsonSerializer =
                    new DataContractJsonSerializer(
                        typeof(T));

                dataContractJsonSerializer.WriteObject(
                    fileStream,
                    dataObject);

            } catch {
                return false;
            }
            return true;
        }

        /// <summary>
        /// <para><see cref="DataContractJsonSerializer"/> を使用して <see cref="object"/> をデシリアライズします。</para>
        /// </summary>
        public static bool DataContractJsonDeserialize<T>(string filePath, out T dataObject)
        {
            try {

                using FileStream fileStream =
                    new FileStream(
                        filePath,
                        FileMode.Open);

                DataContractJsonSerializer dataContractJsonSerializer =
                    new DataContractJsonSerializer(
                        typeof(T));

                var obj =
                    dataContractJsonSerializer.ReadObject(
                        fileStream);

                dataObject = (T)obj;

            } catch {
                dataObject = default;
                return false;
            }
            return true;
        }

        /// <summary>
        /// <para><see cref="XamlWriter"/> を使用して <see cref="UIElement"/> をシリアライズします。</para>
        /// </summary>
        public static bool XamlSerialize<T>(string filePath, T uiElement)
        {
            try {

                //string xamlString =
                //    XamlWriter.Save(
                //        uiElement);

                //File.WriteAllText(
                //    filePath,
                //    xamlString);

                using XmlWriter xmlWriter =
                    XmlWriter.Create(
                        filePath,
                        xmlWriterSettings);

                XamlWriter.Save(
                    uiElement,
                    xmlWriter);

            } catch {
                return false;
            }
            return true;
        }

        /// <summary>
        /// <para><see cref="XamlReader"/> を使用して <see cref="UIElement"/> をデシリアライズします。</para>
        /// </summary>
        public static bool XamlDeserialize<T>(string filePath, out T uiElement)
        {
            try {

                //string xamlString =
                //    File.ReadAllText(
                //        filePath);

                //var obj =
                //    XamlReader.Parse(
                //        xamlString);

                //uiElement = (T)obj;

                using XmlReader xmlReader =
                    XmlReader.Create(
                        filePath);

                var obj =
                    XamlReader.Load(
                        xmlReader);

                uiElement = (T)obj;

            } catch {
                uiElement = default;
                return false;
            }
            return true;
        }
    }
}

まとめ

オブジェクトのシリアライズ・デシリアライズには色々ライブラリが出回ってるみたいですけど、最初のとっかかりとしてはやはり標準を使いたい。というわけなので簡単にまとめてみたわけですが...。
今どきWPFでシリアライズの話?もうやり尽くされてますわ。見たいな感じですよね...。(でも、ざっとまとまってるのがなかったから!)

おまけとして。後は、プロパティにつける属性なんかがあったりします。
属性とはなんぞや? という方は XmlElement とかでggると幸せになれるかもです。

ちなみに初投稿。お目汚し失礼しました('ω')ノ

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

[C#] テストのためにプライベートメンバ変数を追加するのは悪か?

現在、結合テスト用兼可視化ツールを作成しているが、例えば以下のようなクラスについてテストする場合について少し疑問が出てきた。

class A
{
  public string Str { get; }
  string Temp_Str { get; }
}

上のクラスをテストする際に、最終出力であるStrの中間出力であるTemp_Strをテストもしくは可視化したい場合、Temp_Strをprivateとしてではあるがメンバ変数とするのはどうなのだろうか。当然、外からprivateにはアクセスできないので、実際にはこのクラスに対してリフレクション経由で取得しているのだが、本来不要であるはずの中間出力をメンバ変数にするのはいささかどうなのだろうか。

もちろん、実際は単純なstring型ではなく、情報を格納しているclassである。
テストという意味では、中間出力をまとめたクラスを別途作成してそれに対してテストを行なうか、中間出力しているクラスに対してテストを行なうなど方法はいくつかあるが、あえてメンバ変数にしてテストするのもまた選択肢の一つではあるのかなぁとも思いつつも、テストのためにコードを汚すのもいかがなものかと思う。

テストという意味では、最終出力のみをテストすればいいわけで、中間出力なんて別に必要ないはず。ただ、可視化という意味では、中間出力があった方が便利だとも思うわけで、少し堂々巡り気味。外から見た時に、privateでは何も変化がないわけだし…。

テスト難しい。テストの為の設計と可読性は両立できるものなのだろうか。

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

Outsystemsで、いろいろなループの実行時間を計測してみた

やったこと・背景

以前の投稿で、if文を使ってループを回す方法と、C#のRange()のようなアクションを自作し、それを使ってForEachでループを回す方法の2種類を試してみました。
しかし、自作したRangeアクションだとループを2つまわすことになり、性能に問題があるような気がしたので、それぞれの方法(+α)の実行時間を計測しました。
ついでにForEachの時間も計りました。
また、ExtensionでFor文を実行するのと、ServiceStudioで作ったアクションでループを回すのとで、実行時間にどれほど差が出るのか気になったので試してみました。

実行時間は、Forgeで提供されているTicksというComponentを使って計測しました。

環境

  • Outsystems11 (Personal Environment)
  • Traditional Web

計測方法・計測結果

100万回ループを回しました。
10回ずつ計測を行い、平均値を結果としています。
また、それぞれループの回数分1を足すという処理を行っています。

if文でループを回す

image.png

実行時間:1.600秒(100万回)

Rangeアクションを使ってループを回す

image.png
RangeアクションをServiceStudioで作った場合と、Extensionで作った場合で実行速度が結構違いました。

RangeアクションをServiceStudioで作った場合

Ragneアクションは以下のように実装しました。
image.png

実行時間:3.924秒(100万回)

RangeアクションをExtensionで作った場合

Rangeアクションは以下のように実装しました。
OutputParameterは、Integer型の項目のみを持つStructureのリストです。

Range.cs
public void MssRange(int ssStart, int ssLength, out RLIntegerRecordList ssIntegerList) {
    ssIntegerList = new RLIntegerRecordList();
    // TODO: Write implementation for action

    var tmpRecord = new RCIntegerRecord();

    for (int i = 1; i <= ssLength; i++) {
        tmpRecord.ssSTInteger.ssInteger = ssStart;
        ssIntegerList.Add(tmpRecord);
        ssStart += 1;
    }
 } // MssRange

実行時間:2.792秒(100万回)

ForEachだけの実行時間

Rangeアクションを実行した後~ForEachが終わるまでの時間も計ってみました。
実行時間:1.515秒

Extensionの中でfor文を実行

100万回だと値が小さすぎてうまく計測できなかったので、このパターンだけ10憶回ループを回しました。(ちなみにこれ以外のループでは、10憶回回すと余裕でタイムアウトしてしまいます。)
image.png

ForExtension.cs
public void MssForExtension(int ssLength, out int ssSum)
{
    // TODO: Write implementation for action
    ssSum = 0;
    for (int i = 0; i < ssLength; i++)
    {
        ssSum += 1;
    };
} // MssForExtension

実行時間:2.230秒(10憶回)

やりたかったこと

最初、C#のRange()をラッピングしたExtensionを作れば今回自作したRangeアクションよりも早そうだと思ったのですが、ExtensionのOutputParameterに純粋なInt型のリストを設定できないので(やり方を知らないだけかも)結局、Range()で生成したシーケンスをRecordListの型にキャストしなきゃいけないということがわかりました。が、(For文を使わずに)RecordListの型にキャストするのは難しそうだったので断念しました…。

まとめ

if文 Range (ServiceStudio) Range (Extension) ForEach Extension
1.600秒(100万回) 3.924秒(100万回) 2.792秒(100万回) 1.515秒(100万回) 2.230秒(10憶回)

100万回のループで1秒以上かかるのはちょっと遅いなあと思いました。C#だけで実装した場合と全然速さが違いますね。

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

Outsystemsで、いろいろなループの処理時間を計測してみた

やったこと・背景

以前の投稿で、if文を使ってループを回す方法と、C#のRange()のようなアクションを自作し、それを使ってForEachでループを回す方法の2種類を試してみました。
しかし、自作したRangeアクションだとループを2つまわすことになり、性能に問題があるような気がしたので、それぞれの方法(+α)の処理時間を計測しました。
ついでにForEachの時間も計りました。
また、ExtensionでFor文を実行するのと、ServiceStudioで作ったアクションでループを回すのとで、処理時間にどれほど差が出るのか気になったので試してみました。

処理時間は、Forgeで提供されているTicksというComponentを使って計測しました。

環境

  • Outsystems11 (Personal Environment)
  • Traditional Web

計測方法・計測結果

100万回ループを回しました。
10回ずつ計測を行い、平均値を結果としています。
また、それぞれループの回数分1を足すという処理を行っています。

if文でループを回す

image.png

計測結果:1.600秒(100万回)

Rangeアクションを使ってループを回す

image.png
RangeアクションをServiceStudioで作った場合と、Extensionで作った場合で実行速度が結構違いました。

RangeアクションをServiceStudioで作った場合

Ragneアクションは以下のように実装しました。
image.png

計測結果:3.924秒(100万回)

RangeアクションをExtensionで作った場合

Rangeアクションは以下のように実装しました。
OutputParameterは、Integer型の項目のみを持つStructureのリストです。

Range.cs
public void MssRange(int ssStart, int ssLength, out RLIntegerRecordList ssIntegerList) {
    ssIntegerList = new RLIntegerRecordList();
    // TODO: Write implementation for action

    var tmpRecord = new RCIntegerRecord();

    for (int i = 1; i <= ssLength; i++) {
        tmpRecord.ssSTInteger.ssInteger = ssStart;
        ssIntegerList.Add(tmpRecord);
        ssStart += 1;
    }
 } // MssRange

計測結果:2.792秒(100万回)

ForEachだけの実行時間

Rangeアクションを実行した後~ForEachが終わるまでの時間も計ってみました。
計測結果:1.515秒

Extensionの中でfor文を実行

100万回だと値が小さすぎてうまく計測できなかったので、このパターンだけ10憶回ループを回しました。(ちなみにこれ以外のループでは、10憶回回すと余裕でタイムアウトしてしまいます。)
image.png

ForExtension.cs
public void MssForExtension(int ssLength, out int ssSum)
{
    // TODO: Write implementation for action
    ssSum = 0;
    for (int i = 0; i < ssLength; i++)
    {
        ssSum += 1;
    };
} // MssForExtension

計測結果:2.230秒(10憶回)

やりたかったこと

最初、C#のRange()をラッピングしたExtensionを作れば今回自作したRangeアクションよりも早そうだと思ったのですが、ExtensionのOutputParameterに純粋なInt型のリストを設定できないので(やり方を知らないだけかも)結局、Range()で生成したシーケンスをRecordListの型にキャストしなきゃいけないということがわかりました。が、(For文を使わずに)RecordListの型にキャストするのは難しそうだったので断念しました…。

まとめ

if文 Range (ServiceStudio) Range (Extension) ForEach Extension
1.600秒(100万回) 3.924秒(100万回) 2.792秒(100万回) 1.515秒(100万回) 2.230秒(10憶回)

100万回のループで1秒以上かかるのはちょっと遅いなあと思いました。C#だけで実装した場合と全然速さが違いますね。

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