MAUI Blazor

以前、Blazor Hybridの例で、Windows FormsとBlazorを試してみたが、今回はMAUIで試してみた。

MAUIの場合も、Windows Formsと同様にMAUI~Blazor間の連携が可能。

共有する、Singletonインスタンスを以下のような感じで作成し、MauiProgram.cs内でサービス登録することで、相互で同じインスタンスを共有できる。

namespace MAUIBlApp;

public interface IHostIF {
    /// <summary>
    /// 共有データ
    /// </summary>
    /// <value>カウント値</value>
    public int Count { get; set; }
    /// <summary>
    /// イベントハンドラ
    /// </summary>
    public event EventHandler ComponentEvent;
    /// <summary>
    /// クライアント側からイベント発生
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public void FireClientEvent(object sender, EventArgs e);
    /// <summary>
    /// ホスト側からクライアントの画面更新
    /// </summary>
    /// <value></value>
    public Action RefreshClient { get; set; }
}
/// <summary>
/// IHostIFの実装
/// </summary>
public class HostInterface : IHostIF {
    public int Count { get; set; } = 0;
    public event EventHandler ComponentEvent = null!;
    public void FireClientEvent(object sender, EventArgs e) {
        ComponentEvent?.Invoke(sender,e);
    }
    public Action RefreshClient {get; set; } = null!;
}
using Microsoft.Extensions.Logging;

namespace MAUIBlApp;

public static class MauiProgram
{
	public static MauiApp CreateMauiApp()
	{
		var builder = MauiApp.CreateBuilder();
		builder
			.UseMauiApp<App>()
			.ConfigureFonts(fonts =>
			{
				fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
			});

		HostInterface hst = new HostInterface();
		builder.Services.AddSingleton<IHostIF>(hst);
		builder.Services.AddMauiBlazorWebView();

・・・
namespace MAUIBlApp;

public partial class App : Application
{
	public App(IHostIF hif)
	{
		InitializeComponent();
		// MainPageのロード(IHostIF(Singleton)インスタンスを渡す)
		MainPage = new MainPage(hif);
	}
}
public partial class MainPage : ContentPage
{
	private readonly IHostIF _hostif;
	/// <summary>
	/// コンストラクタ(IHostIFのインスタンスを取得)
	/// </summary>
	/// <param name="hostif"></param>
	public MainPage(IHostIF hostif)
	{
		InitializeComponent();
		_hostif = hostif;
		// Client(Blazor Component)イベントハンドラ設定
		_hostif.ComponentEvent += OnClientEvent;
		lblNumber.Text = _hostif.Count.ToString();
	}
	/// <summary>
	/// ボタンクリック時処理
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	public void OnBtnClick(object sender, EventArgs e) {
		// インターフェースのカウントアップ
		_hostif.Count++;
		lblNumber.Text = _hostif.Count.ToString();
		// Client(Blazor Component)のリフレッシュ
		if (_hostif.RefreshClient != null) {
			_hostif.RefreshClient();
		}
	}
	/// <summary>
	/// Client(Blazor Component)イベントハンドラ
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	public void OnClientEvent(object? sender, EventArgs e) {
		lblNumber.Text = _hostif.Count.ToString();
	}
}
@page "/"

@inject IHostIF HostInterface

<h2>Counter</h2>

<p role="status">Current count:
@if (Numbers != null && Numbers.Count != 0) {
    foreach(var v in Numbers) {
        string imgname = $"/images/number_{v}.png";
        <img src="@imgname" style="width:42px;height:49px"/>
    }
}
</p>
<br/>

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

@code {

    /// <summary>
    /// カウンタのイメージIndex
    /// </summary>
    /// <returns></returns>
    protected List<int> Numbers = new () {0};

    /// <summary>
    /// Initialize
    /// </summary>
    protected override void OnInitialized() {
        // コンポーネント側を外部からリフレッシュするActionの設定
        HostInterface.RefreshClient = async () => await InvokeAsync(()=>{CreateNumberImages();StateHasChanged();});
    }
    /// <summary>
    /// ボタンイベント
    /// </summary>
    private void IncrementCount()
    {
        // Interfaceのカウントをインクリメント
        HostInterface.Count++;
        // カウンタ⇒イメージ番号の作成
        CreateNumberImages();
        // ホストに通知
        HostInterface.FireClientEvent(this,new EventArgs());
    }
    /// <summary>
    /// イメージ番号の生成
    /// </summary>
    private void CreateNumberImages() {
        Numbers = new();
        string s = HostInterface.Count.ToString();
        for(int i=0;i < s.Length; i++) {
            Numbers.Add(Convert.ToInt32(s[i].ToString()));
        }
    }
}

以下のように、MAUI側からでもBlazorコンポーネント側からでも同じインスタンスを参照していることが分かる。

最初、MainPageの呼出でコンパイルエラーが出ていて悩んだが、よくよくエラーを見ると、MainPage.xaml.csでは無く、App.xaml.csでMainPageインスタンスを作成するときに、インターフェイスインスタンスを渡していないエラーだった。

Appのコンストラクタにインターフェイスインスタンスをパラメータで指定することにより、サービス登録されたSingletonインスタンスを取得できるので、これをMainPage作成時のパラメータとして渡すことで解決できた。

※MAUIをVS Codeで作成するなら「.NET MAUI拡張」を入れておくと便利。

カテゴリー: .NET, Blazor, C#, MAUI, 技術系 | コメントする

ASP.net WebAPIプロジェクトテンプレート

久しぶりに、WebAPIテンプレートを使ってみたら、デフォルトがuse-minimal-apisになっていて、コードが以下のようにProgram.csに全て入っているコードが生成されてビックリした。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
	・・・
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

今まで通り、Controllerを使用するには、下記のように、dotnet newにオプションを指定する必要があるようだ。

dotnet new webapi -o <プロジェクト名> --use-controllers
#または
dotnet new webapi -o <プロジェクト名> -controllers
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

デフォルトでこちらにして欲しいんだけど・・・

何かminimal apiだと、Node.jsみたいだし、Program.csにコードを全て書くのは好きじゃないなぁ。

カテゴリー: .NET, asp.net core, C#, 技術系 | コメントする

PowershellからOffice365のパスワードをリセットする

PowershllからOffice365 Azure ADのパスワードをリセット(変更)する方法は、MSOnlineモジュールのSet-MsolUserPasswordを使用する方法と、Micorosoft Graph APIモジュールのUpdate-MgUserを使用する方法が一般的だと思う。

Set-MsolUserPasswordでは、下記のように、パスワードを指定しないと、自動でジェネレーとしてくれるので楽。

# MSOnline Module
Set-MsolUserPassword -UserPrincipalName boo@foo.com -ForceChangePassword

ただ、MSOnlineモジュールでは、ライセンス付与のコマンドレット等が非推奨となっており、使用するには色々制限があるので、MSOnlineモジュールを使用して作成したスクリプトをMS Graph APIモジュールに置き換えている最中。

MS Graph APIを使用してパスワードをリセットするには、Update-MgUserコマンドレットを使用する。ユーザー情報そのものの更新になるので、更新用のパラメータボディを作成する必要があるのと、パスワードの自動ジェネレートをサポートしていないので、その部分を用意してあげる必要がある。まぁ、関数化しておくのが無難かな。

# Microsoft Graph API Module
$prm = @{
  passwordProfile = @{
    password = MakePassword # パスワード生成
    forceChangePasswordNextSignIn = $true # 次回サインイン時にパスワード変更を強制
  }
}
Update-MgUser -UserId boo@foo.com -bodyparameter $prm

ちなみに、他人のユーザー情報を更新するには、Connect-MgGraphコマンドレットのscopeに”Directory.ReadWrite.All,Directory.AccessAsUser.All”が必要となる。

忘れない様にメモ。

カテゴリー: Microsoft Graph, PowerShell, 保守・運用, 技術系 | コメントする

サーバー移行しました

今まで使用してきたプラットフォームのOSがCentOS6で、環境を維持するのが色々と面倒になってきたので、WordPressのサーバーを移行しました。

といっても、移行先がCentOS7なので、そのうちまた引っ越すかもしれないが・・・

WordPressの引っ越しは以前仕事で実施したことがあるので、すぐ終わるかと思ったけど、結局半日位かかってしまった。

MySQLのWordPress DB引っ越しはそれほど問題はなかった(と言いつつ、rootのパスワード両サイトとも忘れていたりしたが・・・)のだが、その他の環境等で・・・

特に、引っ越し先のサイトはPHPなんてほとんど使っていなかったので、デフォルトの5.4だったから、色々と面倒が多かった。結局、一度5.4をアンインストールして、8.3をインストールしなおし、拡張機能を追加インストールしてどうにか動作することを確認。

※移行した先のMySQL中のWordPressデータベースのwp_optionsのsiteurlとhomeの2つの修正は忘れないように!!

以上、ご連絡でした。

カテゴリー: 日記的なもの | 1件のコメント

C#12の新機能(一部抜粋)

.NET8でサポートされる、C#12の新機能を調べてみた。

①コレクション式
コレクション式は配列やList等の初期化や代入などに使用できる。
以下のような感じ。

int[] iary_old = new int[] { 1, 2, 3, 4 };	// 従来
int[] iary_new = [ 1, 2, 3, 4 ];	// C#12以降

List<string> slst_old = new List<string>() { "Hello" , "World" };	// 従来
List<string> slst_new = [ "Hello" , "World" ];	// C#12以降

newと初期化を一緒に行うシンタックスシュガーだね。

また、スプレッド演算子”..”というものが使用できる。これを、コレクションの前につけると、コレクションの要素に置き換わる。

> int[] a = [1,2,3];
> int[] b = [4,5,6];
> int[] c = [..a,..b];
> c
int[6] { 1, 2, 3, 4, 5, 6 }

②ラムダ式パラメータのデフォルト値
ラムダ式のパラメータにデフォルト値を設定することができるようになった。

var PlusN = ( int Value, int Addition=1 ) => Value + Addition;

Console.WriteLine($"PlusN(10)={PlusN(10)}");
Console.WriteLine($"PlusN(10,2)={PlusN(10,2)}");
/*	結果
	PlusN(10)=11
	PlusN(10,2)=12
 */
// NGパターン
Func<int,int,int> PlusN = ( Value, Addition=1 ) => Value + Addition;
// error CS9098: 暗黙的に型指定されたラムダパラメーター 'Addition' に既定値を指定することはできません。
Func<int,int,int> PlusN = ( int Value, int Addition=1 ) => Value + Addition;
// warning CS9099: パラメーター 2 のラムダでの既定値は '1' だが、ターゲットデリゲート型では '<missing>' です。

パラメータの型を明示的に指定しないと、デフォルト値の設定は出来ないようだ。
また、型として引数の数を指定してしまうとコンパイル時に警告が出て、実行時にデフォルトパラメータを指定しないと、実行時エラー
CS7036: ‘arg2’ の必要なパラメーター ‘Func’ に対応する特定の引数がありません
となってしまう。

③プライマリコンストラクタ
record型では以前からサポートされていたが、classやstructでも使用できるようになった。

public class Person(string Name, string Mail, DateTime Birthday) {
	public string Name { get; } = Name;
	public string Mail { get; } = Mail;
	public DateTime Birthday { get; } = Birthday;
}

他にもいくつか新機能があるのだが、すぐに使えそうなのはこれぐらいかな。

カテゴリー: .NET, C#, 技術系 | コメントする

Windows Serverをバージョンアップ後サービスが消えた件

Windows Server 2012 R2のEOSに伴い、2012 R2⇒2022にバージョンアップしたところ、一部のサービス登録が消えていた。

該当サーバーではあるプロダクトの一部として、Tomcatを使用しているのだが、OSをバージョンアップしたら、エラーとかは一切出ずに終了したのだが、何故かTomcatサービスの登録だけが消えていた。Tomcatで使用するJAVA_HOME環境変数も同じく。
(そのプロダクトの別サービスは消えていない)

ググってみたが、該当するような記事は見つからない。

とりあえず、JAVA_HOMEをシステム環境変数に登録し、Tomcatもサービス登録して、起動したら、問題無く動いているが、勝手にサービス登録を消されるのは困る・・・

せめて、事前チェックでサポートされていないとかの情報を出力して欲しいものである。

別のサーバーではMS製品しか使用していないので、特にこういう問題は起こっていなかったので、最初は何かと思ったよ。

ん?よくよく調べると、Windows Servre 2012 R2からWindows Server 2022へのインプレースアップグレードって、サポートされていないのか・・・

なら、SETUP実行時に警告かエラー出してくれよ・・・Orz

カテゴリー: 保守・運用 | コメントする

MailKit SMTP送信エラー

アプリケーションからメールを送信するために、以下のような感じでMailKit.Net.Smtpを使用している。

SmtpClient cli = new SmtpClient();
await cli.ConnectAsync("smtp.boo.foo",25);
await cli.SendAsync(msg);

Connectメソッドで、サーバーとポートのみ指定する形。今までは普通に動いていたのだが、SMTPサーバーをIISのSMTPからLinux sendmailに変更したところ、以下のような例外が発生した。

An error occurred while attempting to establish an SSL or TLS connection.
The host name (xxx.xxx.xxx) did not match the name given in the server's SSL certificate (yyy.yyy.yyy).

どうも、START TLSの関係らしい。色々調べてみたが、取敢えず、以下の方法で解決はした。

await cli.ConnectAsync("smtp.boo.foo",25,SecureSocketOptions.None);

しかし、最初の様なコードを持つアプリが結構あるため、SMTPサーバー側でどうにかならないか調べてみたところ、sendmailの場合だと、accessに下記の設定を行なえば、「SMTPサーバーはSTART TLSをサポートしない」という宣言?になるらしい。

# Check the /usr/share/doc/sendmail/README.cf file for a description
# of the format of this file. (search for access_db in that file)
# The /usr/share/doc/sendmail/README.cf is part of the sendmail-doc
# package.
#
# If you want to use AuthInfo with "M:PLAIN LOGIN", make sure to have the
# cyrus-sasl-plain package installed.
#
# By default we allow relaying from localhost...
Srv_Features:   S
・・・

この設定を行ない、sendmailを再起動した後に、元のコードでメール送信を行なったところ、正常にメールが送信された。

社内でしか使用しないSMTPリレーサーバーなので、TLSは必要ないため、これで良しとしよう。

でも、この方法見つけたのは、結構な数のアプリコード直した後だったりする。Orz

カテゴリー: .NET, C#, linux, sendmail, 技術系 | コメントする

.net 8 RC2 blazor dialogタグ

.net 8 RC2からdialogタグのイベントとして、@oncloseと@oncancelが追加された。これにより、ダイアログのクローズ時とESCキーによるキャンセル時にC#コードを呼び出すことができるようになった。

@oncloseはダイアログが閉じた時、@oncancelはESCキーでダイアログを閉じたときに呼び出される。(@oncloseイベントはCancel時でも呼び出されるので、ESCキーでダイアログを閉じた時には、@oncancel⇒@oncloseの順で呼び出される。)

以下に例を挙げる

<button class="btn btn-primary" onclick="dlg.showModal()">ダイアログ表示</button>
<br/>
<hr/>
<pre>@Message</pre>
<hr/>
<dialog id="dlg" @onclose="OnDialog_Close" @oncancel="OnDialog_Cancel">
    <form method="dialog">
        <table>
            <tr>
                <td><span style="font-size:24pt;font-weight:bold">Hello World</span></td>
            </tr>
            <tr>
                <td> </td>
            </tr>
            <tr>
                <td>
                    <button class="btn btn-primary" @onclick="OnOK_Click">OK</button>
                </td>
            </tr>
        </table>
    </form>
</dialog>

@code {
    protected string Message = "";
    int cnt;
    protected override void OnInitialized() {
        cnt = 1;
    }
    protected void OnOK_Click() {
        Message += $"OK Button Click({cnt})->";
    }
    protected void OnDialog_Close() {
        Message += $"Dialog Close({cnt}) ";
        cnt++;
    }
    protected void OnDialog_Cancel() {
        Message += $"Dialog Cancel({cnt})->";
    }
}

以下の実行例のように、OKボタンでクローズされた場合は、@closeで指定されたイベントハンドラのみ実行され、ESCキーで閉じた場合は@cancel⇒@onclickの順でイベントハンドラが実行されている。

まぁ、どちらかと言えば、Dialog中のボタンのハンドラで処理する方が多いかもしれないけれど・・・

カテゴリー: .NET, asp.net core, Blazor, C#, 技術系 | コメントする

.NET 8 RC1 Blazor

.NET 8 RC1(正確にはPreview 6?)からblazorserverプロジェクトテンプレートが無くなってしまった。

.NET Blogによると、

.NET 8 では、Web UI のすべてのニーズに Blazor コンポーネントのフルスタックを使用できるように、Blazor に機能を追加してきました。 要求に応じてサーバーから Blazor コンポーネントを静的にレンダリングし、強化されたナビゲーションとフォーム処理でエクスペリエンスを段階的に強化し、サーバーでレンダリングされた更新をストリーミングし、Blazor Server または Blazor WebAssembly を使用して必要な場合に豊かな対話機能を追加できるようになりました。 アプリの読み込み時間を最適化するために、Blazor は実行時に Blazor Server と Blazor WebAssembly のどちらを使用するかを自動選択することもできます。

とのこと。

Server SideのBlazorコンポーネントを動かすには、blazorプロジェクトテンプレートを使用する必要がある。このテンプレートのオプションを見ると、デフォルトでServer Sideを使用するらしい。

-uw, –use-wasm Configures whether to support rendering components interactively in the browser using WebAssembly.
The default value is false.
Type: bool
Default: false
-us, –use-server Configures whether to support rendering components interactively on the server via a SignalR WebSocket connection.
The default value is true.

Type: bool
Default: true

で、blazorテンプレートでコンポーネントを作ってみたのだが、イベントのハンドリングとかが全然動作しない。.NET Blogの記事を見てもよくわからないし、他の記事でもRC1以前のものばかりで良く分からない。

仕方ないので、サンプルのCounterコンポーネントを見てみると、先頭に以下のようなディレクティブが・・・

@page "/counter"
@attribute [RenderModeServer]

どうもこの属性を指定しないと、サーバーサイドでレンダリングされないようだ。

とりあえず、この属性を指定したら、コンポーネントが動作するようになった。

結構、破壊的な変更だよね。

カテゴリー: .NET, asp.net core, Blazor, 技術系 | 2件のコメント

T-SQLの小技(メモ)

T-SQLの小技?を一つ。

MySQLでは、以下のようにSELECT文にLIMIT句を指定して、クエリ結果から指定オフセットから指定件数分取得することが可能。

SELECT ・・・ FROM TABLE_NAME ・・・ LIMIT 10,5 -- クエリ結果の5行目から10行分
-- または
SELECT ・・・ FROM TABLE_NAME ・・・ LIMIT 10 OFFSET 5 -- クエリ結果の5行目から10行分

ではT-SQLで同じ事を行なう場合はどうすれば良いのか?

昔はスマートなやり方はなかったが、SQL Server 2012からは以下の構文で取得が可能となったようだ。(但し、ORDER BY句の一部だが・・・)

-- クエリ結果の@START_OFFSET行目から@FETCH_NUMBER行分取得
SELECT COLUMN_1[,COLUMN_2[,...]] FROM TABLE_NAME
  ・・・
  ORDER BY KEY_COLUMN
  OFFSET @START_OFFSET ROWS
  FETCH NEXT @FETCH_NUMBER ROWS ONLY

※↓EFでSQL Serverに対して.Skipや.Takeを使用すると、上記と同様なSQLが作成された。

var q = ctx.TABLE_NAME.OrderBy(v=>v.KEY_COLUMN).Skip(n).Take(m);
SELECT ・・・
      FROM TABLE_NAME AS [T]
      ORDER BY [T].[KEY_COLUMN]
      OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

これって結構使う機能だと思うのだけど、T-SQLでサポートされていたとは知らなかった(^^;

カテゴリー: T-SQL, 技術系 | 2件のコメント