LINE Botのリッチメニューを作成するVisual Studio拡張 「XAML Rich Menu Maker」をリリースしました

はじめに

以前このブログで紹介したLINE BOTのリッチメニューをXAMLでデザインするツールVisual Studio拡張機能としてリリースしました!

LINE Developer Community

ソースコードGitHubの 「LINE Developer Community」というOrganization で公開しています。 github.com github.com

ここには他にも、 LINE BOTやClovaスキル等 LINE プラットフォームを利用したアプリケーションの開発に使えるC# .Net Standard )のライブラリが多数収録されています。こちらも合わせてご活用ください!

  • LINE Messaging API
  • LINE Pay
  • LINE Login
  • Clova Extensions Kit
  • LIFF (LINE Front-end Framework のBlazor用C#ラッパー)

※ なお、現在はC#のライブラリ・ツールがメインですが、他の言語・開発環境のリポジトリも随時追加して行く予定です。
「自分のライブラリ・ツール等も登録してほしい」という方も募集中です!
興味のある方はLINE Developer Communityまで!

インストール

Visual Studio Market Place からVSIXファイルをダウンロードしてインストールする場合

  • 以下のサイトにアクセスして[Download]ボタンをクリックしてVSIXファイル(XRMMVsixProject.vsix)をダウンロードします。 marketplace.visualstudio.com

  • ダウンロードしたVSIXファイルをそのまま実行してください。(起動中のVisual Studioがある場合はいったん終了してください)

Visual Stdio 2019 の「拡張機能の管理」を利用してインストールする場合

  • Visual Studioのメニューバーで[拡張機能]-[拡張機能の管理]をクリックし、「Manage Extensions」ダイアログを開きます

    f:id:pierre3:20191122182034p:plain
    メニューバー

  • ダイアログ右上の検索エリアに「XAML Rich Menu Maker」と入力して検索

    f:id:pierre3:20191122181800p:plain:w640
    Manage Extensions

  • 検索結果に「XAML Rich Menu Maker」が表示されたら「Download」ボタンをクリックします

使い方

XAML Ritch Menu Makerプロジェクトを作成する

  • Visual Studio のメニューバーで[ファイル]-[新規作成]-[プロジェクト]をクリックし、「新しいプロジェクトの作成」ダイアログで「LINE Ritch Menu Maker」を選択して「次へ」をクリック

    f:id:pierre3:20191123151714p:plain:w640
    プロジェクト作成ウィザード

  • 任意のプロジェクト名を入力して「作成」をクリックするとXAML Ritch Menu Makerのプロジェクトが作成されます。

  • プロジェクトが作成されたら、メニュー[ビルド]-[ソリューションのビルド]で一度だけビルドを実行します。

BOTアカウントの設定を行う

リッチメニューを利用するBOTアカウントの設定を行います。

  • ソリューションエクスプローラで「appsettings.json」をクリックして編集画面を開き、LINE Bot のチャネルアクセストークンを入力します。
  • また「DebugUserId」には、LINE Developers の「チャネル基本設定」画面の「その他」「Your user ID」に記載されている自分のユーザーIDを設定します(こちらは必須ではないです)。
{
    "AppSettings": {
        "ChannelAccessToken": "+CiR37Hw0xxxxxxxxxxxxxxxxx",
        "DebugUserId": "U28aa2xxxxxxxxxxxxxxxxx"
    }
}

XAMLエディタでリッチメニューをデザインする

リッチメニュー定義ファイル(XAML)を作成する

  • ソリューションエクスプローラで「RichMenuDefs」フォルダを右クリック
  • 表示されるメニューで[追加 ▶] [新しい項目...]をクリック
  • 新しい項目の追加ダイアログで「LINE Rich Menu Definition (XAML)」を選択、ファイル名を入力して「追加」

f:id:pierre3:20191123155107p:plain:w640
新しい項目の追加

以下のXAMLファイルが作成されます。これを編集してリッチメニューのデザインを行います。

<local:RichMenuDefsControl 
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:XamlRichMenuMaker;assembly=XamlRichMenuMaker"
             xmlns:system="clr-namespace:System;assembly=System.Runtime"
             mc:Ignorable="d" 
             d:DesignHeight="1000" d:DesignWidth="2600">
    <UserControl.Resources>
        <system:Double x:Key="RichMenuWidth">2500</system:Double>
        <system:Double x:Key="RichMenuShortHeight">843</system:Double>
        <system:Double x:Key="RichMenuNormalHeight">1686</system:Double>
    </UserControl.Resources>

    <Grid Width="{StaticResource RichMenuWidth}" 
                  Height="{StaticResource RichMenuShortHeight}" >
        <Grid x:Name="menu_body" Background="LightGray">
            <local:RichMenuProperties.Settings>
                <local:RichMenuSettings Name="" ChatBarText="" Selected="True"/>
            </local:RichMenuProperties.Settings>

            <!-- Design your rich menu layout here. -->

        </Grid>
    </Grid>
</local:RichMenuDefsControl>

リッチメニューの設定

リッチメニューの高さ

リッチメニューのサイズは一番外側のGridのWidth,Heightで指定されています。
ファイルを作成した段階では、Shortサイズの高さが指定されています。大きいサイズのリッチメニューを作成したい場合はHeightをRichMenuNormalHeightに変更してください。

<Grid Width="{StaticResource RichMenuWidth}" 
                  Height="{StaticResource RichMenuNormalHeight}" >

</Grid>
リッチメニュー全体の設定を行う

リッチメニューの画像として保存されるのは、”x:Name=menu_body”とタグ付けされたGridコントロールです。 このGridに対して以下の値を設定します。

  • Grid の Background プロパティにGridの背景色を指定します。
  • メニュー登録時の各種設定を添付プロパティRichMenuProperties.Settings で指定します。 指定する項目には以下があります。

    • Name: リッチメニュー名
    • ChatBarText: LINEのチャット画面下のメニューバーに表示されるテキスト
    • Selected: デフォルトでリッチメニューを表示状態にする場合にTrueを指定
リッチメニューの外観をデザインする

リッチメニューの外観は Grid ”menu_body" の中にコントロールを配置することでデザインします。

ここからは作成したプロジェクトのRichMenuDefsフォルダに入っているSampleMenu.xmlを例に説明します。 f:id:pierre3:20191123164143p:plain:w800

まずGridで2つのボタン領域を決め、そこにImageコントロールでアイコン画像を、TextBlockでボタンのキャプションを設定します。

<!-- 列を2つ作り、1つ目と2つ目の幅の比を2:1に設定 -->
<Grid.ColumnDefinitions>
    <ColumnDefinition Width="2*"/>
    <ColumnDefinition/>
</Grid.ColumnDefinitions>

<!-- 1つ目のボタンの画像とキャプション -->
<Image Source="../Resources/cat.png"
            VerticalAlignment="Top"
            Margin="0,150,0,0" Width="420" Height="420"/>
<TextBlock Margin="0,0,0,80" VerticalAlignment="Bottom" 
                HorizontalAlignment="Center" 
                FontSize="80">
        Hello! LINE BOT!
</TextBlock>

<!-- 2つ目のボタンの画像とキャプション -->
<Image Grid.Column="1"  Source="../Resources/web.png"
            VerticalAlignment="Top"
            Margin="236.333,180,237,0" Width="360" Height="360"/>
<TextBlock Grid.Column="1" Margin="0,0,0,80" VerticalAlignment="Bottom" 
                HorizontalAlignment="Center" 
                FontSize="80">Web Site
</TextBlock>
アクションエリアを設定する

次に、リッチメニューの中でボタンとして機能する領域(=アクションエリア)を定義します。 定義する項目は以下の2つです。

  • Bounds: アクションエリアの座標
  • Action: エリアをタップした際に動作するアクション

Boundsには、x:Name="area_{n}" (nには1~20の番号を指定)でタグ付けしたコントロールの座標をそのまま使用します。
サンプルの例では、ボタンの境界の表示も兼ねてRectangle コントロールを使用していますが、別のコントロールでも構いません。

領域をタップした際に動作するActionは 上記コントロールに添付プロパティRichMenuProperties.Actionを指定することで定義します。

<!-- Action Area 1 -->
<Rectangle x:Name="area_1"
        Stroke="DarkGray" StrokeThickness="4">
    <local:RichMenuProperties.Action>
        <local:RichMenuAction Type="Postback" Label="Hello!" Data="Hello!" Text="Hello! LINE BOT!"/>
    </local:RichMenuProperties.Action>
</Rectangle>

Actionには次の4タイプが指定できます。

  • Postback
  • Message
  • URI
  • Datetimepicker

各アクションに必要な設定項目については以下をご参照ください。
アクションオブジェクト: Messaging API リファレンス

Messaging APIを用いてリッチメニューを作成する

XAMLでのデザインが完了したら、LINE Messaging APIを使用してリッチメニューを作成します。

[Ctrl]+[F5]キーを押してアプリケーションを実行します。
アプリケーションが起動すると、以下の様にXAMLでデザインしたリッチメニューの画像と、Messaging APIでLINE側に送信するJSONの内容が表示されます。 f:id:pierre3:20191123171414p:plain:w640

内容を確認してOKなら「Create Rich Menu」ボタンをクリックします。
これでリッチメニューの作成とアップロードの完了です!

作成・登録済みのリッチメニューを確認する

[Create Rich Menu]ボタンの右側にあるコンボボックスを開くと、XAMLファイルで定義したリッチメニュー名(XAMLファイル名)の下に既に登録済みのリッチメニューID(”richmenu-”で始まる文字列)が表示されます。

f:id:pierre3:20191123173031p:plain

これらを選択することで、リッチメニューの内容(画像、JSON)を確認することができます。

また、選択したリッチメニューに対して以下操作が可能です。

  • [Set Default Rich Menu] : Botアカウントの既定のリッチメニューとして設定します
  • [Link to User] : (appsettings.jsonでDebugUserIdに指定した)デバッグ用のLINEユーザーに関連付けします
  • [Unlink from User]: デバッグ用のLINEユーザーから関連付けを外します
  • [Delete Rich Menu] : リッチメニューを削除します

C#のソースコードからPlant UMLのクラス図を生成するツール PlantUmlClassDiagramGenerator v1.2.0 をリリースしました

溜め込んでしまっていたPull Requestを幾つかマージしました。また、ターゲットを.NET Core 3.0 に変更しています。

github.com

以下からダウンロードしてお試いただけます。

.Net Core Global Tools

Visual Studio Code Extension

更新内容

主な変更は以下の通りです。

Null許容型のクラスメンバに対応しました。

以下のように?を付けたNullable型は変換できなかったのですが、変換できるようになりました。

[C#]

class ClassA
{
    public int? PropA { get; set; }
}

[PlantUML]

class ClassA
{
    + PropA : int?
}

All in One オプションを追加しました

フォルダ内のCSファイルを一括で変換した場合、include.puml ファイルに !include で出力した全ファイルの参照を登録して全部のクラス図を一枚に収めることができるようになっています。

@startuml
!include .\\ClassA.puml
!include .\\ClassB.puml
!include .\\ClassC.puml
@enduml
...

しかし、環境によっては !include ディレクティブが利用できない場合があるため、パラメータに -allInOne オプションを付けた場合は include.puml にファイルの参照ではなくファイルの中身をそのまま書き込まれるようになります。

@startuml
class ClassA{
}
class ClassB{
}
class ClassC{
}
@enduml

Blazorクラスライブラリ内で使用するアセット(JS,CSSファイル等)をWebアプリ側に配置するには

本記事は、Blazorで使えるLIFF(LINE Front-end Framework)のC#ライブラリを実装した際の実装メモです pierre3.hatenablog.com

Blazor用のクラスライブラリを作る

Blazor 用のクラスライブラリを作成する際、Microsoft.AspNetCore.Blazor.Templates の バージョン 3.0.0-preview7.19365.7 に入っていた「Blazor library」テンプレートを使用していました。

f:id:pierre3:20190928200903p:plain:w640

しかし、preview8以降このテンプレートは無くなっていて、現在は「Razorクラスライブラリ」テンプレートを使用するようです。

f:id:pierre3:20190927225126p:plain:w640

そこで、今回作成したライブラリも以下のドキュメントを参考に「Blazorクラスライブラリ」テンプレートに置き換えてみました。

docs.microsoft.com docs.microsoft.com

プロジェクトの構成的自体にあまり変わりは無かったのですが、以下の部分で問題がありました。

Razorクラスライブラリ側のアセットがWebアプリ配置先の「dist」フォルダに出力されない

テンプレートから作成したプロジェクトは以下のような構成になっています。

f:id:pierre3:20190928093712p:plain

wwwroot に配置した JSやCSSのファイルが、参照元のWebアプリ(Blazor WebAssembly)側にコピーされて利用可能となる想定です。 しかし、Webアプリを実行してもライブラリ側のJSファイルが認識されませんでした。

Blazor WebAssemblyのプロジェクトをビルドすると「bin\(Debug | Release)\dist」配下にアプリの一式が配置されます。 ライブラリ側のアセットファイルもこの下に格納されると思ったのですが、入っていませんでした。

また、「発行...」でフォルダに配置し、配置先の「.\publish」フォルダを確認してみたところ アプリ配置先の .\publish\{Webアプリのアセンブリ名}\dist フォルダには配置されず、「publish」フォルダ直下に作成された「wwwroot」の中に格納されていました。

  • publish
    • BlazorAppSample
      • dist
        • _content←ここに出力されてほしい
          • RazorClassLib
            • sample.js
            • sample.css
        • _framework
        • index.html
    • wwwroot
      • _content ←ここに出力される
        • RazorClassLib
          • sample.js
          • sample.css

このあたりの正しい構成方法についてはもう少し調べてみたいですが、ひとまずは古いテンプレートの設定に戻して対処することとします。

ライブラリ側のコンテンツをdistフォルダに出力させる

Razorクラスライブラリで以下の修正を行います。

  • ライブラリ側の「wwwroot」フォルダ名を「contents」に変更する
  • ライブラリ側のプロジェクトファイル(.csproj)に以下のコードを追加する
<ItemGroup>
    <!-- .js/.css files will be referenced via <script>/<link> tags; other content files will just be included in the app's 'dist' directory without any tags referencing them -->
    <EmbeddedResource Include="content\**\*.js" LogicalName="blazor:js:%(RecursiveDir)%(Filename)%(Extension)" />
    <EmbeddedResource Include="content\**\*.css" LogicalName="blazor:css:%(RecursiveDir)%(Filename)%(Extension)" />
    <EmbeddedResource Include="content\**" Exclude="**\*.js;**\*.css" LogicalName="blazor:file:%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

これでWebアプリ側の「dist」フォルダ内に出力されるようになります。

Blazor WebAssembly のJS相互運用で、引数にコールバック関数を受け取る関数を実行する方法について

本記事は、Blazorで使えるLIFF(LINE Front-end Framework)のC#ライブラリを実装した際の実装メモです pierre3.hatenablog.com

JavaScriptの関数をC#から実行する

Microsoft.JSInterop.IJSRuntime インターフェースを利用します。
基本的には、JSRuntime.InvokeAsync メソッドに実行したいJSの関数名を渡すだけでOKです。
関数名はグローバルスコープ(window)を基準とした名前です(window.liff.init() の場合 liff.initを渡す)

 public async Task CloseWindowAsync()
            => await JSRuntime.InvokeAsync<object>("liff.closeWindow").ConfigureAwait(false);

引数がある場合

引数がある場合、引数と同じ名前のプロパティを持ったオブジェクトを渡します。 匿名オブジェクトで渡すのが簡単です。

public async Task OpenWindowAsync(string url, bool external)
            => await JSRuntime.InvokeAsync<object>("liff.openWindow", new { url, external }).ConfigureAwait(false);

戻り値がある場合

戻り値がある場合、ジェネリクスの型引数に指定すればその型で受け取ることができます。(デシリアライズしてくれる)

[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
public class Profile
{
    public string UserId { get; set; }
    public string DisplayName { get; set; }
    public string PictureUrl { get; set; }
    public string StatusMessage { get; set; }
}
public async Task LoadProfileAsync()
            => Profile = await JSRuntime.InvokeAsync<Profile>("liff.getProfile").ConfigureAwait(false);

非同期関数の場合

戻り値にPromiseを返してくれるメソッドの場合、そのままInvokeAsyncで実行するだけで良いです。

  • 実行結果(Promise.then() で受け取れるオブジェクト)を戻り値に返してくれる
  • JS側でエラーとなった場合、C#では例外が投げられ、Exception.Messageプロパティでエラー内容( Promise.catch()で受け取れるオブジェクト)が確認できる
public async Task LoadProfileAsync()
            => Profile = await JSRuntime.InvokeAsync<Profile>("liff.getProfile").ConfigureAwait(false);

コールバック関数を受け取る関数の場合

liff.init()関数が、非同期実行の結果を引数に渡したコールバック関数で受け取るようになっています。

当初は以下の様に、C#側に記述したコールバック関数をJS側で呼ぶようにしていました。

JavaScript側からC#で記述したコールバック処理を実行する(却下)
  • liff.init()に渡したコールバック関数内でC#側で定義した関数を呼ぶJavaScriptの中継コードを用意
window.liffInterop = {
    init: function (dotNet) {
        liff.init(
            function (data) {
                dotNet.invokeMethod('OnInitSuccess', JSON.stringify(data));
            },
            function (error) {
                dotNet.invokeMethod('OnInitError', JSON.stringify({
                    code: error.code,
                    message: error.message,
                    stack: error.stack
                }));
            }
        );
    }
};
  • C#側では、コールバックで実行したい処理をイベントに登録
  • イベントを実行するメソッドOnInitSuccessOnInitError[JSInvokable]でマーク
  • JSInvokableなメソッドを持つオブジェクト(LiffClient)のインスタンスDotNetObjectRefにラップしてJSRuntime.InvokeAsyncの引数に渡す
class LiffClient : ILiffClient
{
    public event EventHandler<InitSuccessEventArgs> InitSuccess;
    public event EventHandler<LiffClientErrorEventArgs> InitError;

    public async Task InitializeAsync(IJSRuntime jSRuntime)
    {
        await JSRuntime.InvokeAsync<object>("liffInterop.init", DotNetObjectRef.Create(this));
    }

    [JSInvokable]
    protected void OnInitSuccess(string data)
    { 
        Data = JsonConvert.DeserializeObject<LiffData>(data);
        InitSuccess?.Invoke(this, new InitSuccessEventArgs(Data));
    }

    [JSInvokable]
    protected void OnInitError(string error)
    {
        Error = error;
        InitError?.Invoke(this, new LiffClientErrorEventArgs(Error));
    }
}

これでも動くことは確認したのですが、大袈裟すぎてとても面倒です。そこで以下の様に対処することとしました。

コールバックを取るJavaScriptの関数をPromise に変換する

中間コードを以下の様に変更します。

window.liffInterop = {
    init: function () {
        return new Promise(function (resolve, reject) {
            liff.init(
                function (data) {
                    resolve(JSON.stringify(data));
                },
                function (error) {
                    reject(error);
                });
        });
    }
};

C#側ではPromise化した関数を呼ぶだけでよくなります。かなりシンプルなコードになりました。 (戻り値 はLiffDataオブジェクトとしてそのまま受け取れるはずなのですが、 デシリアライズが失敗するようなので一旦文字列で受けて自前でデシリアライズしています。)

public async Task InitializeAsync(IJSRuntime jsRuntime)
{
        var json = await jSRuntime.InvokeAsync<string>("liffInterop.init").ConfigureAwait(false);
        Data = JsonConvert.DeserializeObject<LiffData>(json);
}

※ JS相互運用については以下のドキュメントをご確認ください docs.microsoft.com

Blazorで使えるLIFF(LINE Front-end Framework)のC#ライブラリを作りました

はじめに

「技術書典7」で頒布された LINE API HANDBOOK の第3章を「Blazor とAzure Functions で作る LIFFアプリケーション」というタイトルで書きました。

booth.pm

その際に、JavaScriptで書かれたLIFFのSDKをBlazor(client-side)用にC#でラップしたクラスライブラリ「liff-client-csharp」を作ったので紹介します。 github.com

liff-client-csharp

「liff-client-csharp」では、LIFFのクライアントAPIのうち以下の機能をサポートしています。
また、ようやくpreviewが外れた.Net Core 3.0.100 に対応しています。

  • liff.init() LIFFアプリを初期化します。このメソッドを実行すると、LIFF SDKの他のメソッドを実行できるようになります。
  • liff.openWindow() 指定したURLをLINE内ブラウザまたは外部ブラウザで開きます。
  • liff.getAccessToken() 現在のユーザーのアクセストークンを取得します。
  • liff.getProfile() 現在のユーザーのプロフィールを取得します。
  • liff.sendMessages() ユーザーの代わりに、LIFFアプリが開かれているトーク画面にメッセージを送信します。
  • liff.closeWindow() LIFFアプリを閉じます。

(参考)LIFF APIリファレンス

※ LIFFのAPIには、LINE Things がらみのデバイス操作用のAPIも用意されているのですが、本ライブラリでは未対応となっています。

使用方法

1. NuGetでライブラリ LineDC.Liff を取得し、Blazor client-side(WebAssembly)のプロジェクトに追加

Install-Package LineDC.Liff -Version 0.6.0-preview

2. wwwroot/index.html のBody内にスクリプトの参照を追加

<body>
  ...

  <script src="https://d.line-scdn.net/liff/1.0/sdk.js"></script>
  <script src="_content/LineDC.Liff/liffInterop.js"></script>
</body>

3. Startup.cs のConfigureServicesでILiffClientを登録

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<ILiffClient, LiffClient>();
    }
}

4. 使用するページでILiffClientを使用する

各ページでは、@Injectディレクティブを用いることで ILiffClientの機能が利用可能となります。

@inject ILiffClient Liff
public interface ILiffClient
{
    bool Initialized { get; }
    LiffData Data { get; }
    Profile Profile { get; }
    string AccessToken { get; }

    Task InitializeAsync(IJSRuntime jSRuntime);
    Task LoadProfileAsync();
    Task<string> GetAccessTokenAsync();
    Task SendMessagesAsync(object messages);
    Task CloseWindowAsync();
    Task OpenWindowAsync(string url, bool external);
    void Reset();
}

以下のコードは、ページ初期化時のイベント(OnInitializedAsync)でLIFFの初期化と、ユーザープロフィールの取得を行い、取得した情報をページ内に表示する例です。

@page "/"
@inject ILiffClient Liff
@inject IJSRuntime JSRuntime

<div class="card" style="width: 20rem;">
    <img class="card-img" src="@Liff.Profile?.PictureUrl" alt="Loading image..." />
    <div class="card-body">
        <h5 class="card-title">@Liff.Profile?.DisplayName</h5>
        <p class="card-text">@Liff.Profile?.StatusMessage</p>
    </div>
    <ul class="list-group">
        <li class="list-group-item">Language: @Liff.Data?.Language</li>
        <li class="list-group-item">Type: @Liff.Data?.Context.Type</li>
        <li class="list-group-item">ViewType: @Liff.Data?.Context.ViewType</li>
        <li class="list-group-item">UserId: @Liff.Data?.Context.UserId</li>
        @if (@Liff.Data?.Context.Type == ContextType.Utou)
        {
        <li class="list-group-item">UtouId: @Liff.Data?.Context.UtouId</li>
        }
        else if (@Liff.Data?.Context.Type == ContextType.Room)
        {
        <li class="list-group-item">RoomId: @Liff.Data?.Context.RoomId</li>
        }
        else if (@Liff.Data?.Context.Type == ContextType.Group)
        {
        <li class="list-group-item">GroupId: @Liff.Data?.Context.GroupId</li>
        }
    </ul>
</div>

@code{

    protected override async Task OnInitializedAsync()
    {
        try
        {
            await Liff.InitializeAsync(JSRuntime);
            await Liff.LoadProfileAsync();
            StateHasChanged();
        }
        catch (Exception e)
        {
            await JSRuntime.InvokeAsync<object>("alert", e.ToString());
        }
    }
}

実装メモ

本ライブラリを実装した際のTipsをメモしておきます。
既存JavaScript ライブラリのBlazor用C#ラッパーを作る際の参考になれば幸いです。

pierre3.hatenablog.com

pierre3.hatenablog.com

C#のソースコードからPlantUMLのクラス図を作成するツールのバージョンアップとVS Codeの拡張を公開しました!

PlantUmlClassDiagramGenerator v1.1.0

以前に作成してGitHubに公開していた、C#ソースコードからPlantUMLのクラス図を作成するツール「PlantUmlClassDiagramGenerator」にプルリクエストが来ていたので、 久しぶりにバージョンアップしました!

「.Net Core 化したよ!」というプルリクエストをもらったのをきっかけに、.Net Core global tools として公開しています。
Windows以外のプラットフォームでも.Net Core SDKが入っていれば動きます。インストールも超簡単なので、ぜひお試しを!

www.nuget.org

アップデートの内容

オブジェクト間の関連を、プロパティ、フィールドの参照から作るようにしました。

組み込み型以外のフィールド、プロパティが以下の様に「関連」(A --> B)として作成されます。
また、フィールド、プロパティが初期化子で初期さされている場合は「集約」(A o-> B)として作成されます。

class ClassA{
    public IList<string> Strings{get;} = new List<string>();
    public Type1 Prop1{get;set;}
    public Type2 field1;
}

class Type1 {
    public int value1{get;set;}
}

class Type2{
    public string string1{get;set;}
    public ExternalType Prop2 {get;set;}
}
  • PlantUML
@startuml
class ClassA {
}
class Type1 {
    + value1 : int <<get>> <<set>>
}
class Type2 {
    + string1 : string <<get>> <<set>>
}
class "IList`1"<T> {
}
ClassA o-> "Strings<string>" "IList`1"
ClassA --> "Prop1" Type1
ClassA --> "field1" Type2
Type2 --> "Prop2" ExternalType
@enduml

f:id:pierre3:20190303222201p:plain
Association.png

この機能はコマンドライン引数に 「-createAssociation」スイッチを追加することで有効になります。表示するオブジェクト数が多い場合はごちゃごちゃしすぎるのでOFFにした方が良いかもしれません。

Visual Studio Code 拡張機能

ついでに、PlantUmlClassDiagramGeneratorを実行できるVisual Studio Code拡張機能を作りました! 単純にコマンドパレットから実行するだけなのですが、VS CodeにはPlantUMLをプレビュー表示できる拡張機能も公開されており、合わせて活用することでVS CodeによるC#の開発が捗ること間違いないしです!

marketplace.visualstudio.com

Line.Messaging 1.4.1 でQuick ReplyとFlex Messageに対応しました

Line.Messaging v1.4.1 をリリースしました

www.nuget.org

Quick Replyと、(長らくお待たせしておりましたがようやく)Flex Message に対応しました!

各機能の詳細は以下のドキュメントをご確認ください。

Quick Reply の使い方

Quick Replyの使い方は簡単で、各種送信メッセージに追加されたQuick ReplyプロパティにQuickReplyオブジェクトを設定してあげるだけです。

new TextMessage("text", new QuickReply()
{
    Items = new[] 
    {
        new QuickReplyButtonObject(new CameraTemplateAction("カメラ起動")),
        new QuickReplyButtonObject(new CameraRollTemplateAction("カメラロール")),
        new QuickReplyButtonObject(new LocationTemplateAction("位置を指定")),
    }
});

QuickReply ので表示するボタンは、QuickReplyButtonObjectItemsプロパティに配列で設定します。
QuickReplyButtonObject には、ボタンをタップした際の動作をActionプロパティで指定します。 Actionには、これまでテンプレートメッセージ等で使用していたITemplateActionインターフェースを実装するクラスに加え、今回追加されたカメラや、カメラロールを起動するアクションが使用できます。

  • MessageTemplateAction
  • UriTemplateAction
  • PostbackTemplateAction
  • LocationTemplateAction
  • DateTimePickerTemplateAction
  • CameraTemplateAction (New!)
  • CameraRollTemplateAction (New!)

Flex Message の使い方

Flex Messageの実装例を以下に書きましたので、こちらを参考に試してみてください。

LineMessagingApi/FlexMessageSampleApp.cs at master · pierre3/LineMessagingApi · GitHub

なお、ライブラリでは、以下の3種類の実装方法をサポートしています。

1. Flex Message のJSON文字列をそのまま利用する

LineMessagingClientに追加された以下のメソッドを 利用することでJSON文字列を直接指定してメッセージを送れるようになりました。

  • ReplyMessageWithJsonAsync
  • PushMessageWithJsonAsync
  • MulticastMessageWithJsonAsync

この方法は、LINE Developers で公開されている Flex Message Simulatorで生成したJSONをコピーして利用する場合に便利です。

private static readonly string FlexJson =
@"{
  ""type"": ""flex"",
  ""contents"": {
    ""type"": ""bubble"",
    ""direction"": ""ltr"",
    ""hero"": {
      ""type"": ""image"",
      ""url"": ""https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_1_cafe.png"",
      ""size"": ""full"",
      ""aspectRatio"": ""20:13"",
      ""aspectMode"": "
...(省略)
};
private static readonly string TextMessageJson = "{ \"type\" : \"text\", \"text\" : \"I Sent a flex message with json string.\" }";

private async Task ReplyFlexWithJson(MessageEvent ev)
{
    await MessagingClient.ReplyMessageWithJsonAsync(ev.ReplyToken, FlexJson, TextMessageJson);
}

2. オブジェクト初期化子を使ってFlex Messageのオブジェクトを組み立てる

以下の様に、Flexの各オブジェクトのプロパティ値をオブジェクト初期化子内で設定してオブジェクトを組み立てます。
この方法で実装した場合、コードの構造が生成されるJSONと一致するため、JSONを見慣れている人には結果がイメージしやすいかもしれません。

private async Task ReplyFlexWithObjectInitializer(MessageEvent ev)
{

    var restrant = CreateRestrantWithObjectInitializer();
    var news = CreateNewsWithExtensions();
    var receipt = CreateReceiptWithExtensions();

    //バブルメッセージ
    var bubble = new FlexMessage("Bubble Message")
    {
        Contents = restrant
    };

    //カルーセルメッセージ
    var carousel = new FlexMessage("Carousel Message")
    {
        Contents = new CarouselContainer()
        {
            Contents = new BubbleContainer[]
            {
                restrant,
                news,
                receipt
            }
        },
        QuickReply = new QuickReply(new[]
        {
            new QuickReplyButtonObject(new CameraRollTemplateAction("CameraRoll")),
            new QuickReplyButtonObject(new CameraTemplateAction("Camera")),
            new QuickReplyButtonObject(new LocationTemplateAction("Location"))
        })
    };

    await MessagingClient.ReplyMessageAsync(ev.ReplyToken, new FlexMessage[] { bubble, carousel });
}

//バブルコンテナの作成
private static BubbleContainer CreateRestrantWithObjectInitializer()
    {
        return new BubbleContainer()
        {
            Hero = new ImageComponent()
            {
                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_1_cafe.png",
                Size = ComponentSize.Full,
                AspectRatio = AspectRatio._20_13,
                AspectMode = AspectMode.Cover,
                Action = new UriTemplateAction(null, "http://linecorp.com/")
            },
            Body = new BoxComponent()
            {
                Layout = BoxLayout.Vertical,
                Contents = new IFlexComponent[]
                {
                    new TextComponent()
                    {
                        Text = "Broun Cafe",
                        Weight = Weight.Bold,
                        Size = ComponentSize.Xl
                    },
                    new BoxComponent()
                    {
                        Layout = BoxLayout.Baseline,
                        Margin = Spacing.Md,
                        Contents = new IFlexComponent[]
                        {
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new IconComponent()
                            {
                                Url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png",
                                Size = ComponentSize.Sm
                            },
                            new TextComponent()
                            {
                                Text = "4.0",
                                Size = ComponentSize.Sm,
                                Margin = Spacing.Md,
                                Flex = 0,
                                Color = "#999999"
                            }
                        }
                    },
                    new BoxComponent()
                    {
                        Layout = BoxLayout.Vertical,
                        Margin = Spacing.Lg,
                        Spacing = Spacing.Sm,
                        Contents = new IFlexComponent[]
                        {
                            new BoxComponent()
                            {
                                Layout = BoxLayout.Baseline,
                                Spacing = Spacing.Sm,
                                Contents = new IFlexComponent[]
                                {
                                    new TextComponent()
                                    {
                                        Text = "Place",
                                        Size = ComponentSize.Sm,
                                        Color = "#aaaaaa",
                                        Flex = 1
                                    },
                                    new TextComponent()
                                    {
                                        Text = "Miraina Tower, 4-1-6 Shinjuku, Tokyo",
                                        Size = ComponentSize.Sm,
                                        Wrap = true,
                                        Color = "#666666",
                                        Flex = 5
                                    }
                                }
                            }
                        }
                    },
                    new BoxComponent(BoxLayout.Baseline)
                    {
                        Spacing = Spacing.Sm,
                        Contents = new IFlexComponent[]
                        {
                            new TextComponent()
                            {
                                Text = "Time",
                                Size = ComponentSize.Sm,
                                Color = "#aaaaaa",
                                Flex = 1
                            },
                            new TextComponent()
                            {
                                Text = "10:00 - 23:00",
                                Size = ComponentSize.Sm,
                                Wrap = true,
                                Color = "#666666",
                                Flex=5
                            }
                        }
                    }
                }
            },
            Footer = new BoxComponent()
            {
                Layout = BoxLayout.Vertical,
                Spacing = Spacing.Sm,
                Flex = 0,
                Contents = new IFlexComponent[]
                    {
                        new ButtonComponent()
                        {
                            Action = new UriTemplateAction("Call", "https://linecorp.com"),
                            Style = ButtonStyle.Link,
                            Height = ButtonHeight.Sm
                        },
                        new ButtonComponent()
                        {
                            Action = new UriTemplateAction("WEBSITE", "https://linecorp.com"),
                            Style = ButtonStyle.Link,
                            Height = ButtonHeight.Sm
                        },
                        new SpacerComponent()
                        {
                            Size = ComponentSize.Sm
                        }
                    }
            },
            Styles = new BubbleStyles()
            {
                Body = new BlockStyle()
                {
                    BackgroundColor = ColorCode.FromRgb(192, 200, 200),
                    Separator = true,
                    SeparatorColor = ColorCode.DarkViolet
                },
                Footer = new BlockStyle()
                {
                    BackgroundColor = ColorCode.Ivory
                }
            }
        };
    }

}

3. ヘルパーメソッドを使ってメソッドチェーンでFlex Messageオブジェクトを生成す

BubbleContainerクラスやBoxComponentクラスに付加された拡張メソッドを利用することで、以下の様にメソッドチェーンで記述することも可能です。
こちらの方が多少すっきりと記述できると思います。

private async Task ReplyFlexWithExtensions(MessageEvent ev)
{
    var restrant = CreateRestrantWithObjectInitializer();
    var news = CreateNewsWithExtensions();
    var receipt = CreateReceiptWithExtensions();

    //バブルメッセージ
    var bubble = FlexMessage.CreateBubbleMessage("Bubble Message")
        .SetBubbleContainer(restrant);

    //カルーセルメッセージ
    var carousel = FlexMessage.CreateCarouselMessage("Carousel Message")
        .AddBubbleContainer(restrant)
        .AddBubbleContainer(news)
        .AddBubbleContainer(receipt)
        .SetQuickReply(new QuickReply(new[]
        {
            new QuickReplyButtonObject(new CameraTemplateAction("Camera")),
            new QuickReplyButtonObject(new LocationTemplateAction("Location"))
        }));

    await MessagingClient.ReplyMessageAsync(ev.ReplyToken, new FlexMessage[] { bubble, carousel });
}

//バブルコンテナの生成
private static BubbleContainer CreateNewsWithExtensions()
{
    return new BubbleContainer()
        .SetHeader(BoxLayout.Horizontal)
            .AddHeaderContents(new TextComponent("NEWS DIGEST") { Weight = Weight.Bold, Color = "#aaaaaa", Size = ComponentSize.Sm })
        .SetHero(imageUrl: "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_4_news.png",
                size: ComponentSize.Full, aspectRatio: AspectRatio._20_13, aspectMode: AspectMode.Cover)
            .SetHeroAction(new UriTemplateAction(null, "http://linecorp.com/"))
        .SetBody(boxLayout: BoxLayout.Horizontal, spacing: Spacing.Md)
            .AddBodyContents(new BoxComponent(BoxLayout.Vertical) { Flex = 1 }
                .AddContents(new ImageComponent("https://scdn.line-apps.com/n/channel_devcenter/img/fx/02_1_news_thumbnail_1.png")
                { AspectMode = AspectMode.Cover, AspectRatio = AspectRatio._4_3, Size = ComponentSize.Sm, Gravity = Gravity.Bottom })
                .AddContents(new ImageComponent("https://scdn.line-apps.com/n/channel_devcenter/img/fx/02_1_news_thumbnail_2.png")
                { AspectMode = AspectMode.Cover, AspectRatio = AspectRatio._4_3, Size = ComponentSize.Sm, Gravity = Gravity.Bottom }))
            .AddBodyContents(new BoxComponent(BoxLayout.Vertical) { Flex = 2 }
                .AddContents(new TextComponent("7 Things to Know for Today") { Gravity = Gravity.Top, Size = ComponentSize.Xs, Flex = 1 })
                .AddContents(new SeparatorComponent())
                .AddContents(new TextComponent("Hay fever goes wild") { Gravity = Gravity.Center, Size = ComponentSize.Xs, Flex = 2 })
                .AddContents(new SeparatorComponent())
                .AddContents(new TextComponent("LINE Pay Begins Barcode Payment Service") { Gravity = Gravity.Center, Size = ComponentSize.Xs, Flex = 2 })
                .AddContents(new SeparatorComponent())
                .AddContents(new TextComponent("LINE Adds LINE Wallet") { Gravity = Gravity.Bottom, Size = ComponentSize.Xs, Flex = 1 }))
        .SetFooter(BoxLayout.Horizontal)
            .AddFooterContents(new ButtonComponent() { Action = new UriTemplateAction("More", "https://linecorp.com") });
}