Quantcast
Channel: ささいなことですが。
Viewing all 104 articles
Browse latest View live

C#だけでJavaScriptを書く! Blazor.DynamicJSを作りました ③DispatchProxy

$
0
0

2023/03/03(今日ですね) にある meetup app osaka@7 で話すやつです。
meetupapp.connpass.com

dynamic で JavaScriptを書けるのですが、それではインテリセンスも使えないし再利用するためにはラップするなりで型をつけてやった方がいいですね。それで interface を定義して定義するだけで使えるようにしました。内部的には DispatchProxy を使っています。こんな感じで書けます。

<script>class Rectangle {        constructor(height, width){this.height = height;this.width = width;}        getArea(){returnthis.height * this.width;}}function sum(...theArgs){let total = 0;for(const arg of theArgs){            total += arg;}return total;}</script>
[JSCamelCase]
publicinterface IWindow
{
    int Sum(paramsint[] val);
}

[JSCamelCase]
publicinterface IRectangle
{
    int Height { get; set; }
    int Width { get; set; }
    int GetArea();
}

privateasync Task Test()
{
    usingvar js =await JS.CreateDymaicRuntimeAsync();

    //メソッドvar window = js.GetWindow<IWindow>();
    varvalue= window.Sum(1, 2, 3, 4, 5);

    //クラス生成var rect = js.New<IRectangle>("Rectangle", 5, 7);
    var area = rect.GetArea();
}

ルール

ここでインターフェイスを定義するとき問題がいくつかあって

  1. C#JavaScriptでは命名規則が違う場合がある
  2. 非同期どうするか
  3. new とかそもそも interface で表せないもの

いくつか対応するためのルールを追加しています。

名前に対するルールです。

//camel case になる、メソッド、プロパティ単位の指定も可能
[JSCamelCase]
publicinterface ITest
{
    //インデックスアクセスできるintthis[int index];
    
    // valueになるint Value { get; set; }

    // sum(,,,args) になる int Sum(paramsint[] values);

    // 文字列指定 JSNameで指定した名前が使われます
    [JSName("getData2")]
    int GetData(int x);
}

プロパティ、new をメソッドに変換できます。

publicinterface ITest
{
    //new Rectangle になります
    [JSConstructor("Rectangle")]
    IRectangle CreateRectangle();
    
    //Valueになります
    [JSProperty("Value")]
    int GetValue();
    [JSProperty("Value")]
    void SetValue(intvalue);
    
    //this[index]
    [JSIndexProperty]
    int GetAt(int index);
    [JSIndexProperty]
    void SetAt(int index, intvalue);
}

非同期は Task を返すように定義することで非同期になります。名前の末尾にAsyncをつけると自動でそれは削除されます。つけたい場合は JSName を併用するとそっちが優先されます。プロパティも↑の属性でメソッドに置き換えることにより非同期で使えるようになります。

publicinterface ITest
{
    //GetValueを非同期で呼び出します
    Task<int> GetValueAsync();
}

Handsontable

それで前回のHandsontableも class と interface で書いてみました。

publicclassCol
{
    publicobject? Data { get; set; }
    publicobject? ReadOnly { get; set; }
    publicobject? Width { get; set; }
    publicobject? ClassName { get; set; }
    publicobject? Type { get; set; }
    publicobject? NumericFormat { get; set; }
}

publicclassProductMaster
{
    publicstring? Edit { get; set; }
    publicbool Select { get; set; }
    publicstring? ProductCode { get; set; }
    publicstring? ProductName { get; set; }
    publicint UnitPrice { get; set; }
    publicstring? Comment { get; set; }
}

publicclassEnterMoves
{
    publicint Row { get; set; }
    publicint Col { get; set; }
}

publicclassInitialData
{
    publicobject[]? Data { get; set; }
    publicstring[]? ColHeaders { get; set; }
    public Col[]? Columns { get; set; }
    public EnterMoves? EnterMoves { get; set; }
    publicbool OutsideClickDeselects { get; set; }
    publicbool ManualColumnResize { get; set; }
    publicbool FillHandle { get; set; }
}

[JSCamelCase]
publicinterface IHandsontable
{
    void LoadData(List<ProductMaster> data);
    void SetDataAtCell(int row, int col, string data);
}

publicinterface IAfterChangesInfo
{
    dynamicthis[int index] { get;set; }
}

[JSCamelCase]
publicinterface IAfterChangesInfoArray
{
    IAfterChangesInfo this[int index] { get; set; }
    int Length { get; set; }
}

publicstaticclassHandsontableExtentions
{
    publicstatic IHandsontable CreateHandsontable(this DynamicJSRuntime js, ElementReference grid, InitialData data, Action<IAfterChangesInfoArray, dynamic> afterChange)
    {
        var arg = js.ToJS(data);
        arg.afterChange = afterChange;
        return js.New<IHandsontable>("Handsontable", grid, arg);
    }
}
@page "/"
@using Blazor.DynamicJS
@inject IJSRuntime JS
<div @ref="_grid"></div>

@code {
    private ElementReference _grid;

    protectedoverrideasync Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;
        var js =await JS.CreateDymaicRuntimeAsync();
        dynamic window = js!.GetWindow();

        conststring COL_EDIT ="edit";
        conststring COL_SELECT ="select";
        conststring COL_PRODUCTCODE ="productCode";
        conststring COL_PRODUCTNAME ="productName";
        conststring COL_UNITPRICE ="unitPrice";
        conststring COL_COMMENT ="comment";
        conststring EDIT_MARK ="*";

        IHandsontable? hot =null;
        hot = js.CreateHandsontable(_grid,
            new InitialData
            {
                Data =new object[0],
                ColHeaders =new[] { "編集", "選択", "商品CD", "商品名", "単価", "備考" },
                Columns =new Col[]
                {
                new Col{ Data = COL_EDIT, ReadOnly =true, Type ="text" },
                new Col{ Data = COL_SELECT, Type ="checkbox" },
                new Col{ Data = COL_PRODUCTCODE, Type ="text", Width =80 },
                new Col{ Data = COL_PRODUCTNAME, Type ="text", Width =200, ClassName ="htLeft htMiddle" },
                new Col{ Data = COL_UNITPRICE, Type ="numeric", NumericFormat =new { pattern ="0,00", culture ="ja-JP" } },
                new Col{ Data = COL_COMMENT, Type ="text", Width =300, ClassName ="htLeft htMiddle" }
                },
                EnterMoves =new EnterMoves { Row =0, Col =1 },
                OutsideClickDeselects =true,
                ManualColumnResize =true,
                FillHandle =false,
            },
            (changes, source) =>{
                if (source =="loadData") return;

                for (var i =0; i < (int)changes.Length; i++)
                {
                    var change = changes[i];
                    // 編集と選択は対象外if (change[1] == COL_EDIT || change[1] == COL_SELECT) continue;
                    // 変更前と変更後が同じは対象外if (change[2] == change[3]) continue;
                    // 編集に"*"を付ける
                    hot?.SetDataAtCell((int)changes[0][0], 0, EDIT_MARK);
                }
            }
        );

        hot.LoadData(new List<ProductMaster>()
        {
            { new ProductMaster() { Edit ="", Select =false, ProductCode ="S0001", ProductName ="りんご", UnitPrice =100, Comment ="青森産" } },
            { new ProductMaster() { Edit ="", Select =false, ProductCode ="S0002", ProductName ="みかん", UnitPrice =80, Comment ="静岡産" } },
            { new ProductMaster() { Edit ="", Select =true,  ProductCode ="S0003", ProductName ="メロン", UnitPrice =1000, Comment ="袋井クラウンメロン" } }
        });
    }
}

ちょっと残念なのは = まだはうまく表現できないので source と changesの 末端は dynamic のままにしています。


だいたいなんでもExcelをPDFに変換する魔法

$
0
0

すみません、フリーレン風に言いたかっただけです。Nugetのライブラリです。
というわけで作りました。MITライセンスでサーバーでも、なんならWebAssemblyでブラウザ上でも使えます。
github.com

変換できるのは

  1. 文字(サイズ、太さ、色)
  2. セル(種類、色、マージ)
  3. 画像

これだけなんですけど、一般的に使う分にはこれだけいけたら十分じゃない?リクエストあればサクッと作れるものなら足していこうと思います。

GitHubのReadMeにも載せてますがこんな感じの変換ができます。

おまけ機能で簡単な文字置換ができます。これを使えば元ネタをExcelで作っておいて、それをデータに合わせて書き換えてPDFに変換ってことができます。

使い方なんですけど、Fontがちょっと面倒。これは内部的に使ってるPdfSharpの仕様です。Fontって勝手に再配布できないし、そもそもサイズが大きいのでライブラリには取り込めない。なので使う分だけ用意してもらう仕様です。とはいえ業務アプリで使う場合はそんなフォントこだわりないんじゃないかな?この例ではNotoSansで統一して太字だけ対応してます。用途に合わせて調整してみてください。

publicclassCustomFontResolver : IFontResolver
{
    publicbyte[] GetFont(string faceName)
        //Implement so that you can get as many fonts as you need.=> faceName.EndsWith("#b") ? Resources.NotoSansJP_ExtraBold : Resources.NotoSansJP_Regular;

    public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic)
    {
        var faceName = familyName; 
        if (isBold) faceName +="#b";
        returnnew FontResolverInfo(faceName);
    }
}
GlobalFontSettings.FontResolver =new CustomFontResolver();

それが終わると後は変換するだけです。ファイルパスを渡すのと、Webアプリで使いやすいようにStreamを渡す版も作ってます。

usingvar outStream = ExcelConverter.ConvertToPdf(workbookPath, 1);
File.WriteAllBytes(pdfPath, outStream.ToArray());

作り方

ClosedXMLとOpenXmlで解析してPdfSharpで書いているだけです。だから意外と小規模なコードです。幅と高さの単位を合わせるのがめっちゃ大変だった・・・。そのへん@tmytに教えてもらいました。ありがとう!

Codeer.LowCode.Blazorをリリース!(してました

$
0
0

5/13にCodeer.LowCode.Blazorというライブラリをリリースしました!有料なんですけど評価版は無料で申請なく使えるんで使ってみてね。テンプレートも作っててサクッと試すことができます。
marketplace.visualstudio.com

ライセンスのお見積り、ご相談はこちらからお願いします!

実行エンジン型のローコード機能をBlazorアプリに組み込むライブラリです。

ローコードアプリって色々あるけどライブラリってたぶん世界初なんちゃうかな。以前に「CSパフォーマンス勉強会」でLTしたのが動画を撮ってくれてたんで参考までに(ありがとうございます!)4:48:40からですね、リンクそこにしてるけどいけるんかな。
www.youtube.com

  • うちのアプリにもローコード機能あったらなー
  • 大体は一般的なローコードアプリでいいけど、特別に組み込みたい機能あるんよね
  • お客さんから「既製品使ってよ、でもこれとあれとそれはできないと困る」って無理言われること多くなったよな
  • Webアプリ開発は苦手なんでとりあず大部分はローコードで作れたらいいのに、そしたらクラサバからWebアプリにリプレイスしやすいよね

とかありますよね、わかります。僕もそう思ったんで作りました。

テンプレで新規作成してビルドするとWebアプリとWPFのDesignerアプリができてそのDesingerで画面/機能を作っていくことができるのです!もちろんそれぞれ拡張可能となっています。

ノー/ローコードとプロコードのいいとこどり

一旦言葉の整理。Codeer.LowCode.Blazorでは以下の定義にしてます。(あくまでこの製品ではってことで

ノーコードデザイナで設定
ローコードデザイナでスクリプト実装
プロコード普通に.NETやBlazorのコードで実装

大部分はデザイナの設定だけでサクッと作ってこだわりの部分はBlazorと.NETでお手軽にいつも通りに実装する。自分のアプリなんでスコープは小さいしめっちゃ融通効く。まさにこれが僕が実現したかったことなんです。

ノー/ローコードだけでも多くのプロジェクトは作れます。

かなり頑張りました!

ポトペタで画面作成

グリッドレイアウト、フローレイアウト、キャンバスレイアウトを組み合わせて画面が作れます。フィールドも標準的なものはそろってます。標準的でないやつは後で紹介するプロコードで増やしていけます。そういうのもサンプルコード的に順次公開していきます。

DBとの連携

一般的なCRUDはもちろんJOINや1Nの関係の表現も可能です。TableだけでなくViewにも関連付けることができるのでBI機能も簡単に実現。変更履歴も簡単に残せるようにしています。 その他検索/論理削除/楽観ロック/作成更新情報など一般的にDBの操作で必要になるものは取り揃えていて多くの既存システムのDBと連携できます。検索機能も充実しています。

身に覚えのあるスクリプトの書き味


イベントハンドラや同期的なダイアログの表示、UIの有効/無効、表示/非表示の制御など.NET開発者が一度は書いたことがあるであろう書き味で書けます。WebAPIも呼べるしエクセルの制御だってできちゃいます。もちろんこれもプロコードでAPIを増やしていくことができます。インテリセンスももちろん使えます(頑張った)。サンプルのようにC#っぽく書けるので、「プロコードでの実装と何が違うのですか?」って聞かれました。当然の疑問ですよね。以下のような感じで使ってもらうことを想定しています。

  • スクリプトなのでアプリのビルドなくデザイナだけで変更可能
  • デザイナで作ったModuleやFieldは基本スクリプトで制御
  • 簡単なことはこっちの方が書きやすい
  • デバッガは使えないので複雑なことは非推奨、難しいことはプロコードで公開したAPIに任せる

プロコードでの拡張

様々な方法で拡張できるようにしています。ライブラリでの組み込み型なのでそもそものアプリの部分はかなり自由に改造できます。

  • コードビハインド
  • 画面全体をrazorで実装
  • 画面の一部をrazorで実装
  • Fieldを実装してポトペタで配置できるようにする
  • スクリプトAPIを公開
  • そもそものアプリ部分を調整

詳細な機能もブログで紹介していきます。

いや、GitHubにもドキュメント作っていってるんですけどね。公式ドキュメントって堅苦しくなるし、考えすぎてなぜかわかりにくくなるんですよね・・・。(とは言え、そっちも頑張ります
場合によってはサンプル交えつつ雑記的に書いた方がわかりやすくなる場合もあるので、こっちも色々書いていきます。

実はクラサバも作れます。Codeer.LowCode.Blazor

$
0
0

Codeer.LowCode.Blazorは、その名の通りBlazorアプリにローコード機能を組み込むためのライブラリです。BlazorはBlazorWebViewを使えばWPFやWinFormsで動かすことができます。つまり、Codeer.LowCode.Blazorを利用することで、WPFやWinFormsにローコード機能を組み込むことが可能です。さらに、WindowsアプリなのでUIだけでなく、DB操作ロジックも組み込めるため、クラサバアプリも作成できます。しかも無理やり組み込む感じではなくテンプレも用意したんで本当に簡単に作れます!

Web版の概要

Codeer.LowCode.Blazorは、ブラウザ側ではUIのロジックが、サーバー側ではDB操作のロジックが動作します。これらの間はWebApiで接続されています。WebApiの部分はライブラリ外ですが、テンプレートを使用して新規作成する際に自動で設定されます。

クラサバ版の概要

実際のところ、ライブラリ部分に大きな違いはありません。WebApiで接続するか、インプロセスのロジックで接続するかの違いだけです。これもテンプレートを使用して簡単に作成できます。

クラサバアプリにローコードを組み込んでみよう

クラサバアプリでよく聞くのは、基本部分は作っているけど、細かいカスタマイズはお客様ごとにハードコードで対応しているという話です。Codeer.LowCode.Blazorを使えば、そのハードコードの部分を大幅に工数削減できます。テンプレで生成したコードを元にあなたのアプリに組み込んでください。もしくは別プロセスにしてプロセス連携も良いと思います。

クラサバアプリからのリプレイスのつなぎに最適

Codeer.LowCode.Blazorは、WinFormsなどのクラサバアプリからWebアプリへのリプレースを支援することもメインの目的の一つです。ドラッグ&ドロップで画面を作成したり、大部分をデザイナーの設定やスクリプトで作成したりすることで、Blazorの機能を活用しつつスキルのアンマッチによる負担を軽減します。しかし、一気にリプレースするのは大変です。まずはWinFormsにCodeer.LowCode.Blazorを組み込み、少しずつ置き換えていきます。これにより、将来的にWebアプリに移行する際もそのまま活用できるのです。

お問合せはこちらから!

https://www.codeer.co.jp/LowCode
よろしくお願いします!

ちなみにトライアルは無料で使えます。テンプレからサクッと作れますので是非試してみてください。

Viewing all 104 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>