.NET MAUI Blazor With Radzen!

初めまして、アキちゃんです。

突然ですが皆さんは、デスクトップアプリケーションを作成するときにどのプラットフォームを選択しますか?

僕はWPFのData Bindingが嫌いすぎてWinFormを選択し続けてきました。

ですが、WinFromってデザインが良くも悪くもシステムチックなんですよね。

なのでRadzenが使えるMAUIに乗り換えようと思います。

それでは早速・・・.NET MAUI Blazor with Radzen

Lesson.1 プロジェクトの準備

まずは、MAUIのプロジェクトを作成します。

新しいプロジェクトの作成から.NET MAUI Blazorアプリを選択します。

プロジェクト名はMauiBlazorWithRadzenで行きます。

フレームワークは.NET8.0を選択します。

ここまで来たら完了でプロジェクトができると思います。

Lesson.2 Radzen導入

プロジェクトが作成できたらNugetでRadzenをインストールします。

あとは以下のサイトにならって設定を書くだけですが、まあせっかくなので手順を詳しく書いていこうと思います。

www.radzen.com

wwwroot\index.htmlに以下の変更を加えます。

<link rel="stylesheet" href="css/app.css" />の前の行に<link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css" />を追加。

<script src="_framework/blazor.webview.js" autostart="false"></script>の次の行に<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>を追加。

index.html(クリックで展開) index.html(クリックで圧縮)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
    <title>MauiBlazorWithRadzen</title>
    <base href="/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="_content/Radzen.Blazor/css/humanistic-base.css">
    <link rel="stylesheet" href="css/app.css" />
    <link rel="stylesheet" href="MauiBlazorWithRadzen.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
</head>

<body>

    <div class="status-bar-safe-area"></div>

    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.webview.js" autostart="false"></script>
    <script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>

</body>

</html>

_Imports.razorに以下の変更を加えます。

@using Radzen@using Radzen.Blazor を追記します。

_Imports.razor(クリックで展開) _Imports.razor(クリックで圧縮)
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using MauiBlazorWithRadzen
@using MauiBlazorWithRadzen.Components
@using Radzen
@using Radzen.Blazor

Components\Layout\MainLayout.razorにも変更を加えるのですが、

サイト通りだと、

@inherits LayoutComponentBaseの次の行に<RadzenComponents />を追記します。

といった内容が書かれています。

ですがなんかそれだとRadzen導入している感じがなくて切なくなっちゃうので以下の通りに大幅に書き換えます。

MainLayout.razor(クリックで展開) MainLayout.razor(クリックで圧縮)
@inherits LayoutComponentBase
<RadzenComponents />

<RadzenLayout Style=" grid-template-columns: auto 1fr auto; grid-template-areas: 'rz-header rz-header rz-header' 'rz-sidebar rz-body rz-right-sidebar'">
<RadzenHeader>
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="0">
<RadzenRow class="w-100">
<RadzenColumn Size="6">
<div class="d-flex align-items-center">
<RadzenSidebarToggle Click="@(() => sidebar5Expanded = !sidebar5Expanded)" />
<RadzenLabel Text="タイトル">
</RadzenLabel>
</div>
</RadzenColumn>
</RadzenRow>
</RadzenStack>
</RadzenHeader>
<RadzenSidebar @bind-Expanded="@sidebar5Expanded">
<RadzenPanelMenu style="height: 100%;">
<ChildContent>
<RadzenPanelMenuItem Icon="debug" Text="Home" Path="">
</RadzenPanelMenuItem>
<RadzenPanelMenuItem Icon="debug" Text="Counter" Path="counter">
</RadzenPanelMenuItem>
<RadzenPanelMenuItem Icon="debug" Text="Weather" Path="weather">
</RadzenPanelMenuItem>
</ChildContent>
</RadzenPanelMenu>
</RadzenSidebar>
<RadzenBody>
<ChildContent>
<RadzenContentContainer Name="main">
@Body
</RadzenContentContainer>
</ChildContent>
</RadzenBody>
</RadzenLayout>

@code {
int sidebarWidth = 0;
bool sidebar5Expanded = true;

string Status = string.Empty;

bool IsMask = false;

protected override async Task OnInitializedAsync()
{
}
}

MauiProgram.csに以下の変更を加えます。

using Radzen;を追加します。

builder.Services.AddMauiBlazorWebView();の次の行にbuilder.Services.AddRadzenComponents();

を追記します。

MauiProgram.cs(クリックで展開) MauiProgram.cs(クリックで圧縮)
using Microsoft.Extensions.Logging;
using Radzen;

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

#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif

return builder.Build();
}
}
}

Lesson.3 確認

前項まで終わったら実行することで以下画面がでてくると思います。

ここまでできたら完了です。

Lesson.4 感想

皆さんはMAUIどうですかね?

僕的には久しぶりにおっ!と思えるものが出てきて少し震えています。

まあ、そういえるのもBlazorがベースにあってRadzenが使えるからなんだろうけど。

何よりSystem.IO.Ports等のハードウェアに依存するライブラリが今まで通りに使えるのは組み込み屋さんとして非常に助かります。

.NETを入れなければいけないという点において状況によっては使えないかもしれませんが使える場面では積極的に選択していいかな~ってあきちゃん的におすすめしたいですね ♪

では、また次回の記事で!

ちょいちょい忘れるシリーズ①

初めまして、アキちゃんです。

備忘録をかねてブログを書くことにしました。

今回はInvokeについて書きます。

それでは早速・・・インボーーーク

Lesson.1 事の発端

C#を書き始めて7年目に突入しそうな僕ですが、いまだにInvokeをラムダで書くときにぱっと出てこない瞬間があります。

検索すればいくらでもでてくるものですが、設計者としてそれってどうなのお・・・って思ったので記事にしようと思います。

あとなんか理由つけて(記事)書かないと最終更新から1年たちそうだし・・・。

Lesson.2 書き方

Invoke(new Action(() =>
{
 label1.Text = "非同期描画:" + (++cnt);
}));

基本の書き方はこうなります。

使い道は、UIスレッドとは違うスレッドからUIをいじるときにだと思います。

(.Net系は基本スレッドセーフであるので相当特殊なことをしない限りは上記以外に使いどころはないと思います。)

Lesson.3 動き

動作が見えないと安心して使えないと思うので以下に一例を示します。

UserControlにして動かしていますが味噌なのは赤字の部分だけなので、使ってるなあ・・・程度の認識で見ていてください

UcInvoke.cs(クリックで展開) UcInvoke.cs(クリックで圧縮)
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace DebugApp.Sample
{
 public partial class UcInvoke : UserControl
 {
  public CancellationTokenSource TokenSource { get; private set; }
  public UcInvoke()
  {
   InitializeComponent();
  }
  private async void BtnStart_Click(object sender, EventArgs e)
  {
   BtnStart.Enabled = false;
   try
   {
    TokenSource = new CancellationTokenSource();
    await Task.WhenAll(LoopTask(TokenSource.Token));
   }
   catch (TaskCanceledException ex)
   {
   }
   finally
   {
    label1.Text = "待機中";
    TokenSource = null;
   }
   BtnStart.Enabled = true;
  }

  private Task LoopTask(CancellationToken token)
  {
   return Task.Factory.StartNew(() =>
   {
    var cnt = 0;
    while (true)
    {
     // 今回はデバックのため使うが基本的にTaskをWaitしてはならない
     // (UIスレッドのデッドロックが発生する原因となるため)
     
Task.Delay(100).Wait();
     
// このInvokeのラムダ内でUI描画を行う
     
Invoke(new Action(() =>
     
{
      
label1.Text = "非同期描画:" + (++cnt);
     }));
     if (token.IsCancellationRequested)
     
{
      
break;
     
}
    
}
   
});
  
}
  private void BtnStop_Click(object sender, EventArgs e)
  {
   
if (TokenSource == null)
   {
   
}
   
else
   
{
    
TokenSource.Cancel();
   
}
  
}
 }
}

Lesson.4 まとめ

さぼらないでちゃんと更新するようにしなければ・・・

以上!!

Let's BlazorServer+Radzen ♪ ⑧ (2023/06 時点での最新環境)

初めまして、アキちゃんです。

備忘録をかねてブログを書くことにしました。

今回は2023/06 時点での最新環境で変わったことについて書きます。

それでは早速・・・2023/06 時点での最新環境で変わったこと!

Lesson.1 事の発端

久しぶりに新しい社内Webアプリをリリースしようと思ってRadzenについて調べたらいろいろ進化していました。

その中に過去の記事で行っていた面倒な手間が不要となっていたため、せっかくなんで記事にしようと思います。

Lesson.2 プロジェクト作成~Radzen導入まで

プロジェクト作成~Radzen導入までは、今までと特にかわりませんが、Nugetパッケージから取得するRadzenのバージョンのみ現在の最新(4.13.2)にしておきます。

akr9915.hatenablog.com

基調のデザインについてもいろいろ追加されていました。

無料で使えるものの中で、個人的にhumanisticがおしゃれだったので

"Pages\_Layout.cshtml"の

<link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">

を、

<link rel="stylesheet" href="_content/Radzen.Blazor/css/humanistic-base.css">

に修正します。

Lesson.3 公式デザイン導入の簡略化

僕は過去に以下のような記事を書きましたが、その手間が一切なくなりました。

akr9915.hatenablog.com

新しく追加されたLayoutコンポーネントを使用することで、メイン画面にてハンバーガーメニューをより簡単に実装することができるようになりました。

blazor.radzen.com

Shared/MainLayout.razorを、以下のように書き換えます。

MainLayout.razor(クリックで展開) MainLayout.razor(クリックで圧縮)
@inherits LayoutComponentBase
@using Radzen
@using Radzen.Blazor
@using System.Text.Json
@inject NavigationManager Navigation
<RadzenDialog />
<RadzenNotification />
<RadzenTooltip />
<RadzenContextMenu />

<RadzenLayout Style="grid-template-columns: auto 1fr auto; grid-template-areas: 'rz-header rz-header rz-header' 'rz-sidebar rz-body rz-right-sidebar'">
 <RadzenHeader>
 <RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="0">
 <RadzenRow class="w-100">
<RadzenColumn Size="6">
<div class="d-flex align-items-center">
<RadzenSidebarToggle Click="@(() => sidebar5Expanded = !sidebar5Expanded)" />
<RadzenLabel Text="Radzen 4.13.2">
</RadzenLabel>
</div>
</RadzenColumn>
<RadzenColumn Size="6">
<div class="rz-text-align-right">
<RadzenSidebarToggle Click="@(() => sidebar6Expanded = !sidebar6Expanded)" />
</div>
</RadzenColumn>
</RadzenRow>
</RadzenStack>
</RadzenHeader>
<RadzenSidebar @bind-Expanded="@sidebar5Expanded">
<RadzenPanelMenu>
<RadzenPanelMenuItem Text="Home" Icon="home" Path="" />
<RadzenPanelMenuItem Text="Counter" Icon="count" Path="counter" />
<RadzenPanelMenuItem Text="Fetch" Icon="fetch" Path="fetchdata" />
</RadzenPanelMenu>
<div class="rz-p-4">
Left Sidebar
</div>
</RadzenSidebar>
<RadzenBody>
<ChildContent>
<RadzenContentContainer Name="main">
@Body
</RadzenContentContainer>
</ChildContent>
</RadzenBody>
<RadzenSidebar @bind-Expanded="@sidebar6Expanded" Style="width: 300px; grid-area: rz-right-sidebar">
<div class="rz-p-4">
Right Sidebar
</div>
</RadzenSidebar>
</RadzenLayout>
@code {
bool sidebar5Expanded = true;
bool sidebar6Expanded = true;
}

この状態で実行すると以下のような画面がでてきます。

Lesson.4 まとめ

以前までの記事のやり方ですると、IDEを一回入れてプロジェクトを作成し、ソースコードを引っ張て来てプロジェクトを作るたびに手を入れてあげなければいけなかったため手間でしたが、コーディングだけで実装できることがわかり、かなり楽になりました。

またコンポーネントについても、特にレイアウトにかかわる部分が追加・更新されていてcssで頑張っていたところも、コンポーネントが勝手に実装してくれるようになっていました。

以上!!

github.com

Http通信のWrapperクラス

初めまして、アキちゃんです。

備忘録をかねてブログを書くことにしました。

今回はIHttpClientFactoryのWrapperクラスついて書きます。

それでは早速・・・IHttpClientFactoryのWrapperクラス!

Lesson.1 事の発端

BlazorでControllerを使用する際に、基本的にはIHttpClientFactoryを使用すると思います。

これを毎回べたに書くと思いのほか記述が多くなるためWrapperクラスを作ろうと思います。

Lesson.2 HttpWrapper

StatusCode等の応答に対する実装は考慮していませんのでそこは別で実装する必要がありますが、値を取得する・設定する等のシンプルな実装は以下となります。

ざっくりこうです。

public class HttpWrapper
{
        IHttpClientFactory ClientFactory;
        public HttpWrapper(IHttpClientFactory clientFactory)
        {
            ClientFactory = clientFactory;
        }
      public async Task<string> AsyncGetString(string uri)
        {
            var request = new HttpRequestMessage(HttpMethod.Get, uri);
            var client = ClientFactory.CreateClient();
            var response = await client.SendAsync(request);
            return await response.Content.ReadAsStringAsync();
        }
      public async Task<byte[]> AsyncGetByteArray(string uri)
        {
            var request = new HttpRequestMessage(HttpMethod.Get, uri);
            var client = ClientFactory.CreateClient();
            var response = await client.SendAsync(request);
            return await response.Content.ReadAsByteArrayAsync();
        }
       
      public async Task AsyncPost(string uri, MultipartFormDataContent content)
        {
            var request = new HttpRequestMessage(HttpMethod.Post, uri);
            var client = ClientFactory.CreateClient();
            {
                request.Content = content;
                await client.SendAsync(request);
            }
        }
}

別段気を付けることはありませんが、String・Binary以外の戻り値を取る際はresponse.Contentの変数を使い分ける必要があります。

またPost通信のContentについては使用する側から用意する実装としています。

Lesson.3 使い方

使い方は、HttpWrapperの変数を宣言しOnInitializedで@injectしたIHttpClientFactoryのインスタンスを引数にnewしてあげます。

@inject IHttpClientFactory ClientFactory
@code {
    HttpWrapper httpWrapper;
   protected override void OnInitialized()
    {
        httpWrapper = new HttpWrapper(ClientFactory);
    }
}

あとは、ControllerのUrlとか送りたいデータをMultipartFormDataContentにして渡してあげれば大丈夫です。

AsyncGetString

var response = await httpWrapper.AsyncGetString($"https://localhost:7272/get");
var getData = JsonSerializer.Deserialize<List<TClass>>(response );

AsyncPost

var content = new MultipartFormDataContent();
content .Add(new StringContent(JsonSerializer.Serialize<List<TClass>>(TClassData.ToList())), "file", "test.json");
await httpWrapper.AsyncPost($"https://localhost:7272/set", content);
content.Dispose();

Lesson.4 あとがき

以上!!

Blazor Serverで簡易ファイル共有(後編:ファイルダウンロード)

初めまして、アキちゃんです。

備忘録をかねてブログを書くことにしました。

今回はBlazor Serverで簡易ファイル共有(後編:ファイルダウンロード)について書きます。

それでは早速・・・Blazor Serverで簡易ファイル共有(後編:ファイルダウンロード)!

Lesson.1 前回まで

前回まででファイルをアップロードするところまで作成しました。

akr9915.hatenablog.com

今回はアップロードしたファイルをダウンロードするところまでしようと思います

Lesson.2 事前準備

Blazorでファイルをダウンロードする方法は少ないですがいくつかあります。

今回はMicrosoftが紹介している以下サイトの方法で行おうと思います。

learn.microsoft.com

Pages/_Layout.cshtmlに、以下の記述を追記します。

_Layout.cshtml(クリックで展開) _Layout.cshtml(クリックで圧縮)
@using Microsoft.AspNetCore.Components.Web
@namespace FileShare.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html lang="en">
<script>
    window.downloadFileFromStream = async (fileName, contentStreamReference) => {
        const arrayBuffer = await contentStreamReference.arrayBuffer();
        const blob = new Blob([arrayBuffer]);
        const url = URL.createObjectURL(blob);
        const anchorElement = document.createElement('a');
        anchorElement.href = url;
        anchorElement.download = fileName ?? '';
        anchorElement.click();
        anchorElement.remove();
        URL.revokeObjectURL(url);
    }
</script>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="FileShare.styles.css" rel="stylesheet" />
    <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
    @RenderBody()
    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.server.js"></script>
</body>
</html>

Lesson.3 ファイル一覧・ダウンロード

アップロードしたファイル一覧を取得し、任意のファイルをダウンロードするコントローラーを作りたいため、Controllerフォルダに 追加→コントローラー→MVCコントローラー - 空 を選択し、DownloadController.csを作成します。

できたDownloadController.csをざっくり以下のように修正します。

DownloadController.cs(クリックで展開) DownloadController.cs(クリックで圧縮)
using Microsoft.AspNetCore.Mvc;
using System.Text;
using System.Text.Json;
namespace FileShare.Controllers
{
    public class DownloadController : Controller
    {
        private readonly IWebHostEnvironment environment;
        public DownloadController(IWebHostEnvironment environment)
        {
            this.environment = environment;
        }
        [Route("api/getfiles")]
        public IActionResult GetFiles()
        {
            try
            {
                var rslt = new List<string>();
                var files = Directory.GetFiles(Path.Combine(environment.WebRootPath + @"\share"));
                foreach (var i in files)
                {
                    rslt.Add(Path.GetFileName(i));
                }
                return Content(JsonSerializer.Serialize(rslt.ToArray()), "application/json", Encoding.UTF8);
            }
            catch (Exception ex)
            {
                return StatusCode(500, ex.Message);
            }
        }
        [Route("api/download/file")]
        public async Task<IActionResult> DownloadDocument(IFormFile file)
        {
            try
            {
                using (var memoryStream = new MemoryStream())
                {
                    await file.CopyToAsync(memoryStream);
                    byte[] buffer = memoryStream.ToArray();
                    var fileName = JsonSerializer.Deserialize<string>(Encoding.ASCII.GetString(buffer));
                    var filePath = Path.Combine(environment.WebRootPath + @"\share", fileName);
                    return File(System.IO.File.ReadAllBytes(filePath), "application/octet-stream", fileName);
                };
            }
            catch (Exception ex)
            {
                return StatusCode(500, ex.Message);
            }
        }
    }
}

アップロードしたファイル一覧を表示し、任意のファイルをダウンロードする画面を作りたいため、Pagesフォルダに追加→Razorコンポーネントを選択し、Download.razorを作成します。

できたDownload.razorをざっくり以下のように修正します。

Download.razor(クリックで展開) Download.razor(クリックで圧縮)
@page "/download"
@using System.Text.Json
@inject IJSRuntime JSRuntime
@inject IHttpClientFactory ClientFactory
@foreach (var i in files)
{
    <button @onclick="(s => ButtonClick(i))">@i</button>
}
@code {
    List<string> files = new List<string>();
    protected override async Task OnParametersSetAsync()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:7281" + "/api/getfiles");
        var client = ClientFactory.CreateClient();
        var response = await client.SendAsync(request);
        var getVal = await response.Content.ReadAsStringAsync();
        files.AddRange(JsonSerializer.Deserialize<string[]>(getVal));
    }
    async void ButtonClick(string fileName)
    {
        var content = new MultipartFormDataContent();
        content.Add(new StringContent(JsonSerializer.Serialize<string>(fileName)), "file", "file.json");
        var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:7281" + "/api/download/file");
        var client = ClientFactory.CreateClient();
        request.Content = content;
        var response = await client.SendAsync(request);
        var getVal = await response.Content.ReadAsStreamAsync();
        using var streamRef = new DotNetStreamReference(getVal);
        await JSRuntime.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
    }
}

Download.razorにアクセスできるようにするために、Shared/NavMenu.razorを以下のように修正します。

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="upload">
                <span class="oi oi-data-transfer-upload" aria-hidden="true"></span> Upload
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="download">
                <span class="oi oi-data-transfer-download" aria-hidden="true"></span> Download
            </NavLink>
        </div>
    </nav>
</div>

ここまできたら実行しUploadで任意のファイルをアップロードします。(今回は困った18.pngをアップロード)

ファイルアップロード後、Downloadに遷移するとUploadで選択したファイル名のボタンが存在しています。(困った24.pngは前回記事で上げたものです。)

任意のボタンを押下しダウンロードが完了したらアップロードしたファイルがダウンロードされているはずです。

Lesson.4 あとがき

以上!!

github.com

Blazor Serverで簡易ファイル共有(前編:ファイルアップロード)

初めまして、アキちゃんです。

備忘録をかねてブログを書くことにしました。

今回はBlazor Serverで簡易ファイル共有(前編:ファイルアップロード)について書きます。

それでは早速・・・Blazor Serverで簡易ファイル共有(前編:ファイルアップロード)!

Lesson.1 事の発端

たまにファイルを共有したいことがありますよね、なのでアップロードしたファイルを共有できる簡易ファイル共有アプリを作ろうと思います。

Lesson.2 プロジェクト作成(FileShare)

プロジェクト名はFileShareで、Blazor Server プロジェクト(.Net6)で作成します。

基本デフォルトのまま次へ押しとけば大丈夫ですが一応以下記事にまとめています。

akr9915.hatenablog.com

Lesson.3 事前準備

今回は取り敢えずファイルアップロードを実装しようと思います。

作成したプロジェクトのwwwrootの下にshareフォルダを作成します。

プロジェクト直下にControllersというフォルダを作成します。

ファイルのアップロードとダウンロードはControllerから取得したいのでProgram.csに以下のコードを追加します。

app.UseRouting();
app.UseEndpoints(userEndPpoints =>
{
    userEndPpoints.MapControllers();
});
app.MapBlazorHub();

Controllerを使用するためHttpRequestMessageを使用したいため、Program.csに以下コードを追記します。

builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddHttpClient();

var app = builder.Build();

ここまででプロジェクトファイルは以下のようになっていると思います。

Lesson.4 ファイルアップロード 

ファイルをアップロードするコントローラーを作りたいため、Controllerフォルダに 追加→コントローラー→MVCコントローラー - 空 を選択し、UploadController.csを作成します。

できたUploadController.csをざっくり以下のように修正します。

UploadController.cs(クリックで展開) UploadController.cs(クリックで圧縮)
using Microsoft.AspNetCore.Mvc;
namespace FileShare.Controllers
{
    public class UploadController : Controller
    {
        private readonly IWebHostEnvironment environment;
        public UploadController(IWebHostEnvironment environment)
        {
            this.environment = environment;
        }
        [Route("api/upload/file")]
        public async Task<IActionResult> UploadDocument(IFormFile file)
        {
            try
            {
                using (var memoryStream = new MemoryStream())
                {
                    await file.CopyToAsync(memoryStream);
                    var data2 = memoryStream.ToArray();
                    var filePath = Path.Combine(environment.WebRootPath + @"\share", file.FileName);
                    using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
                    {
                        fs.Write(data2, 0, data2.Length);
                        fs.Close();
                    }
                };
                return StatusCode(200);
            }
            catch (Exception ex)
            {
                return StatusCode(500, ex.Message);
            }
        }
    }
}

ファイルをアップロードする画面を作りたいため、Pagesフォルダに追加→Razorコンポーネントを選択し、Upload.razorを作成します。

できたUpload.razorをざっくり以下のように修正します。

Upload.razor(クリックで展開) Upload.razor(クリックで圧縮)
@page "/upload"
@inject IHttpClientFactory ClientFactory
<label style=" height:30px;line-height:30px;color:black;background-color:greenyellow;  border-radius: 5px; ">
    ファイル選択
    <InputFile style="display:none" OnChange="@(args => FileUpload(args))" />
</label>
@code {
    async Task FileUpload(InputFileChangeEventArgs e)
    {
        foreach (var file in e.GetMultipleFiles())
        {
            var ms = new MemoryStream();
            await file.OpenReadStream(file.Size).CopyToAsync(ms);
            ms.Position = 0;
            var fileContent = new StreamContent(ms);
            using var content = new MultipartFormDataContent();
            content.Add(fileContent, "file", file.Name);
            var request = new HttpRequestMessage(HttpMethod.Post, "https://localhost:7281" + "/api/upload/file");
            var client = ClientFactory.CreateClient();
            {
                request.Content = content;
                await client.SendAsync(request);
            }
        }
    }
}

upload.razorにアクセスできるようにするために、Shared/NavMenu.razorを以下のように修正します。

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="upload">
                <span class="oi oi-data-transfer-upload" aria-hidden="true"></span> Upload
            </NavLink>
        </div>
    </nav>
</div>

ここまできたら実行しUploadを選択後、ファイル選択を押下します。

表示された開く画面から任意のファイルを選択し開くorダブルクリックします。

表示された開く画面が閉じた後、プロジェクトのwwwroot/shareフォルダに選択したファイルがアップロードされているはずです。

Lesson.5 あとがき

年明けましたね!

年末年始忙しすぎて全然記事書けなかった!

ソースコードは後編で公開します。

また、記事では使用しないように実装しましたが

OpenReadStream().ReadAsync(buffer)

を使用するとうまく動作しないみたいです。詳しくは以下記事を確認ください。

github.com

github.com

Blazor ServerでExcelに電子印鑑(後編:Excel画像貼り付け)

初めまして、アキちゃんです。

備忘録をかねてブログを書くことにしました。

今回はBlazor ServerでExcelに電子印鑑(後編:Excel画像貼り付け)について書きます。

それでは早速・・・Blazor ServerでExcelに電子印鑑(後編:Excel画像貼り付け)!

Lesson.1 前回まで

前回までで印鑑っぽい画像のBitmapを返すControllerを作成しました。

*2022/12/26に誤りが確認でき、記事のLesson.4の修正を行いました。

akr9915.hatenablog.com

今回はHttpRequestMessageでBitmapを取得しExcelの任意のセルに張り付けるところまでしようと思います。

Lesson.2 NPOI

C#Excelを操作することのできるライブラリは結構たくさんあります。

画像やグラフの操作に関してはNPOIが一番簡潔だと思うので使わせてもらいましょう。

Nugetパッケージの管理からNPOIをインストールします。

Lesson.3 Excel画像貼り付け

前回までに、Stamp画像のStreamを取得できるControllerを作成しました。

なので今回はStreamを渡してExcelに張り付けるControllerを作ろうと思います。

Controllerフォルダに 追加→コントローラー→MVCコントローラー - 空 を選択し、SealStampController.csを作成します。

できたSealStampController.csをざっくり以下のように修正します。

SealController.cs(クリックで展開) SealController.cs(クリックで圧縮)
using Microsoft.AspNetCore.Mvc;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
namespace ExcelStamp.Controllers
{
    public class SealStampController : Controller
    {
        private readonly IWebHostEnvironment environment;
        public SealStampController(IWebHostEnvironment environment)
        {
            this.environment = environment;
        }
        [HttpPost]
        [Route("api/sealstamp")]
        public async Task<IActionResult> SealStamp(IFormFile[] file)
        {
            var guid = Guid.NewGuid();
            try
            {
                var orgFileName = Path.Combine(environment.WebRootPath + @"\excel", "test.xlsx");
                var newFileName = Path.Combine(environment.WebRootPath + @"\delete", $"{guid}.xlsx"); ;
                System.IO.File.Copy(orgFileName, newFileName);
                using (var stampMs = new MemoryStream())
                {
                    await file[0].CopyToAsync(stampMs);
                    var stamp = stampMs.ToArray();
                    var anchor = new XSSFClientAnchor()
                    {
                        AnchorType = AnchorType.DontMoveAndResize,
                        Row1 = 0,
                        Row2 = 4,
                        Col1 = 0,
                        Col2 = 2,
                        Dx1 = XSSFShape.EMU_PER_PIXEL * 5,
                        Dx2 = XSSFShape.EMU_PER_PIXEL * -5,
                        Dy1 = XSSFShape.EMU_PER_PIXEL * 5,
                        Dy2 = XSSFShape.EMU_PER_PIXEL * -5,
                    };
                    SealStamp(newFileName, newFileName, "Sheet1", anchor, stamp);
                }
                return StatusCode(200);
            }
            catch (Exception ex)
            {
                return StatusCode(500, ex.Message);
            }
        }
        protected void SealStamp(string inpath, string outpath, string sheetName, XSSFClientAnchor anchor, byte[] data)
        {
            var book = WorkbookFactory.Create(inpath);
          var sheet = book.GetSheet(sheetName);
            var pictureidx = book.AddPicture(data, (PictureType)XSSFWorkbook.PICTURE_TYPE_PNG);
            var patriarch = sheet.CreateDrawingPatriarch();
            patriarch.CreatePicture(anchor, pictureidx);
         
            using (var fs = new FileStream(outpath, FileMode.OpenOrCreate))
            {
                book.Write(fs, true);
            }
          book.Close();
        }
    }
}

動作としてはwwwroot/excel/text.xlsxを発行したGUID.xlsxでwwwroot/delete/{GUID}.xlsxでコピーを取りそのファイルに画像を貼り付けてStatusCode(200)を返しています。

Excelのシートへの画像貼り付けはRow1,Row2,Col1,Col2で位置調整します。

Lesson.4 HttpRequestMessage

あとは、それぞれ作ったControllerを使用するためHttpRequestMessageを使う準備をします。

Program.csに以下コードを追記します。

builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddHttpClient();

var app = builder.Build();

社名・日付・名前・権限を入力できるようにIndex.razorを以下のように修正します。

Index.razor(クリックで展開) Index.razpr(クリックで圧縮)
@page "/"
@inject IHttpClientFactory ClientFactory
<PageTitle>Index</PageTitle>
<div class="form-group">
    <label>
        会社名:
        <input @bind="compInput" size="50" />
    </label>
</div>
<div class="form-group">
    <label>
        ユーザー名:
        <input @bind="userInput" size="50" />
    </label>
</div>
<div class="form-group">
    <label>
        日付文字列:
        <input @bind="datestrInput" size="50" />
    </label>
</div>
<div class="form-group">
    <label>
        権限文字列:
        <input @bind="powerInput" size="50" />
    </label>
</div>
<button @onclick="Send">作成</button>
@code {
    private string? compInput;
    private string? userInput;
    private string? datestrInput;
    private string? powerInput;
    private async Task Send()
    {
        byte[] stampBinary;
        {
            var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:7009" + "/api/stamp/getstamp/" + compInput + "/" + userInput + "/" + powerInput + "/" + datestrInput);
            var client = ClientFactory.CreateClient();
            var response = await client.SendAsync(request);
            stampBinary = await response.Content.ReadAsByteArrayAsync();
        }
        {
            var request = new HttpRequestMessage(HttpMethod.Post, "https://localhost:7009" + "/api/sealstamp");
            var content = new MultipartFormDataContent();
            content.Add(new ByteArrayContent(stampBinary), "file", "stamp.bin");
            var client = ClientFactory.CreateClient();
            {
                request.Content = content;
                await client.SendAsync(request);
            }
        }
    }
}

赤字のポート番号はお手前のプロジェクトに合わせてください。

ここまで来たら実行してみて以下のように入力して作成を押下します。

するとプロジェクトのwwwroot/deleteにExcelファイルができるのでこれをダブルクリックで開きます。

すると黒印鑑でA1~B4のセルに黒い印鑑画像が貼り付けられていたらここまで完了です。

また権限を1にすることで赤い印鑑画像が貼り付けられます。

Lesson.5 あとがき

前回記事ではソースがないのにミスコードを上げてしまい申し訳なかったです。: (_;´꒳`;) :_

以上・・・

github.com