■WEB/DESKTOPアプリ開発、どれだけ違う? 🤔

表題の件、業務アプリ開発専門&Microsoft信者の視点でお話しします。

①使う側の視点での違い。
🖥 DESKTOPアプリ: 
・.Net Runtimeをインストールすれば、Windows,Linux,iOSで動きます。
・プログラムは、ローカルPCで実行されます。
・プログラムをインストールする必要がある。

🌐 WEBアプリ:
・EdgeやChrome、SafariのWEBブラウザー上で動きます。
・プログラムは、WEBサーバー上で実行されます。(WEBブラウザー内で実行する部分もあり)
・プログラムをインストールする必要は無い。

②開発側の視点での違い。
🖥 DESKTOPアプリ:
・DESKTOPアプリ開発者が開発。
小規模システム向け
・DBへの接続の認証などは必須。
・テスト環境は構築しやすい。
・プログラムソース全体の量は、WEBアプリより少なくなる。
・ユーザーの要望は殆ど実現可能。 😀

🌐 WEBアプリ:
・WEBアプリ開発者が開発。(フロントエンド開発者、バックエンド開発者で別々が基本?)
大規模システム向け
・WEB上にデータが流れるので、ログイン時やデータ接続時の認証は必須。 (AzureのようなWEBサービスを使用するのが必須。)
・テスト環境の構築には、若干手間が必要。
・プログラムソース全体の量は、DESKTOPアプリより多くなる。バックエンドとフロントエンドの別々の開発が必要な為。
 1)バックエンド:サーバー内で動く部分。
2)フロントエンド:WEBブラウザー内で動く部分。
 ↑WEBアプリの構造上、ここは変わらないと思います。
・WEBブラウザーの制限上、できないことが多々ある。 😢

・DeskTopアプリ: ローカルPCで実行するプログラムのみ。

・WEBアプリ: サーバー側とローカル側の2つのプログラム。
★この違いはその後の保守の容易さにも影響してきます。

業務アプリ開発 WEB/Desktop?

■社内向け業務アプリ開発 >「①デスクトップアプリ」 or 「②WEBアプリ」どっちがいい? 🤔

表題の通り、社内向け業務アプリ開発で、そもそも「 🖥 デスクトップ/ 🌐 WEB」で悩むところだと思います。
私の個人的な認識は以下の通りです。
(ちなみに私は業務アプリ開発専門&Microsoft信者ですので。 😊 )

①🖥デスクトップアプリ: 
<メリット>
・アプリの柔軟性が絶大。ローカル環境での他のシステムとの接続が自由。
・動作が軽い。(ローカルPCで動く為)
・画面の設計が自由。操作性がよく直感的に使える。

<デメリット>
・新規・更新の配布の際、自動的にダウンロードするメニューなどを用意する必要あり。
※共有フォルダーなどから、随時ダウンロード。
・社外の人にプログラムを共有するのに、手間が掛かる。
※SharePointなどで、共有etc。

②🌐WEBアプリ:
<メリット>
・Azure Webアプリのように、WEB上に公開できるので、配布が楽。
・不特定多数のユーザーの使用に向いている。
・プログラムの入れ替えが楽。(ただし入れ替え時、一時的にサービスが使えない)

<デメリット>
・WEBなので、セキュリティ対策が必要。(IP制限、AzureAD認証など)
・起動時の認証と、ページ読み込みで、時間が掛かる。
・WEBアプリなので、WEBシステム上の制約が多い。
 ※他のシステムとの連携がやり難い。
・WEBサーバーのスペックがそれなりに必要で、ランニングコスト大。

★社内のユーザーに、同じプログラムを、「①デスクトップ版」と「②WEB版」の両方を作って、どちらが使いやすいか聞いてみました。

私も当然予想はしていましたが、①デスクトップ版の方でした。

試したプログラムは、照会系でデザイン的には全く同じものでした。
(なので、私のWEBのデザインが悪かったというのは無しです。 😓 )

特定の業務向けのアプリの場合は、WEBよりデスクトップアプリの方が向いているようです。 😊
※ちなみに、iPadでやる時は、Webアプリ(Blazor)で開発しています。


#社内システム開発 #VisualStudio

DeskTopで、自作ComboBox!

Desktopアプリ開発で、一番悩むのが、dataGridView関係とcomboBoxかと思います。今回は、コンボボックスの一例を紹介したいと思います。WinFormsには、有料のコンポーネントが数多くありますが、結構なお値段なのと、私の長年の経験として、意外とバグがあったりして、それを補完しながらの開発も結構、工数が増えます。

今回は、コンボボックスについて1パターンを紹介したいと思います。3列のコンボボックスで、データをList<DataCombo>形式で渡して、戻り値として、選択したID(1列目)とNAME(二列目)を取得するユーザーコントロールを作りました。(下記イメージ)

CustomCombo.cs (ユーザーコントロール)クラスを一つ作成します。

↓ユーザーコントロール(コンボボックス)のClassソース

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;

namespace WinForms01
{
    /// --------------------------------------------------------------------------------
    /// <summary>
    /// 作成: モバイルステーション 2024.02.23 M.SAGARA
    /// </summary>
    /// --------------------------------------------------------------------------------
    public partial class CustomCombo : UserControl
    {
        private System.Windows.Forms.TextBox textBox;
        private System.Windows.Forms.Button dropDownButton;
        private Form dropDownForm;
        private DataGridView dataGridView;

        public string sNAME = "";

        // カスタムイベントの定義
        public event EventHandler TextBoxTextChanged;

        /// --------------------------------------------------------------------------------
        /// <summary>
        /// コンストラクター
        /// </summary>
        /// --------------------------------------------------------------------------------
        public CustomCombo()
        {
            InitializeComponent();
            InitializeCustomComboBox();
            InitializeDropDownFormWithDataGridView();
            // dataGridView.CellClickイベントを購読
            this.dataGridView.CellClick += MyTextBox_TextChanged;
        }

        /// --------------------------------------------------------------------------------
        /// <summary>
        /// 外部からデータを受け取るためのメソッド 
        /// ※DataCombo (Id,Name,Descripton)
        /// </summary>
        /// <param name="items"></param>
        /// --------------------------------------------------------------------------------
        public void SetData(List<DataCombo> items)
        {
            dataGridView.DataSource = items;
            dataGridView.Refresh();
        }

        // TextBoxのTextChangedイベントハンドラ
        private void MyTextBox_TextChanged(object sender, EventArgs e)
        {
            // カスタムイベントを発火
            TextBoxTextChanged?.Invoke(sender, e);
        }
        // TextBoxのテキストを外部から取得するためのプロパティ
        public string TextBoxText
        {
            get { return textBox.Text; }
            set { textBox.Text = value; }
        }

        private void InitializeCustomComboBox()
        {
            // TextBoxの設定
            textBox = new System.Windows.Forms.TextBox
            {
                Dock = DockStyle.Fill,
                ReadOnly = true
            };
            this.Controls.Add(textBox);

            // ドロップダウンボタンの設定
            dropDownButton = new System.Windows.Forms.Button
            {
                Dock = DockStyle.Right,
                Text = "▼",
                Width = 24,  // ボタンの幅を24ピクセルに設定
                Height = 24
            };
            this.Controls.Add(dropDownButton);
            dropDownButton.Click += (s, e) => ShowDropDownForm();

            // コントロールの高さ調整
            // ボタンの高さを調整する場合は、CustomComboBoxControlのサイズ調整を行う
            this.Height = textBox.Height; // コントロールの高さを調整
        }

        private void InitializeDropDownFormWithDataGridView()
        {
            // dataGridViewの設定
            dataGridView = new DataGridView
            {
                Dock = DockStyle.Fill,
                SelectionMode = DataGridViewSelectionMode.FullRowSelect,
                AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells,
                AllowUserToResizeRows = false,
                RowHeadersVisible = false, // 行ヘッダーを非表示に設定
                ReadOnly = true,
                AllowUserToAddRows = false
            };
            // DataGridViewの行のデフォルトの高さを設定
            dataGridView.RowTemplate.Height = 18;

            dropDownForm = new Form
            {
                FormBorderStyle = FormBorderStyle.None,
                StartPosition = FormStartPosition.Manual,
                Size = new Size(300, 300),  // 初期サイズ設定
                TopMost = true
            };
            dropDownForm.Controls.Add(dataGridView);
            dropDownForm.Deactivate += (s, e) => dropDownForm.Hide();
            dataGridView.CellClick += DataGridView_CellClick;
        }

        /// --------------------------------------------------------------------------------
        /// <summary>
        /// dataGridViewの部分が表示されるときの、イベント
        /// </summary>
        /// --------------------------------------------------------------------------------
        private void ShowDropDownForm()
        {
            // 現在のスクリーンの作業領域を取得(タスクバーなどを除いた領域)
            Rectangle screenWorkingArea = Screen.GetWorkingArea(this);

            // ComboBoxのスクリーン上の位置を取得
            Point comboBoxScreenLocation = this.textBox.PointToScreen(Point.Empty);

            // Formの予定表示位置(ComboBoxの直下)を計算
            int formX = comboBoxScreenLocation.X;
            int formY = comboBoxScreenLocation.Y + textBox.Height;

            // Formの予定表示位置が画面下からはみ出るかどうかを確認
            bool isBelowScreen = (formY + dropDownForm.Height) > screenWorkingArea.Bottom;

            if (dropDownForm.Visible)
            {
                dropDownForm.Hide();
            }
            else
            {
                if (isBelowScreen)
                {
                    // 画面下からはみ出る場合、FormをComboBoxの上に表示
                    formY = comboBoxScreenLocation.Y - dropDownForm.Height;
                }

                // Formの位置を設定して表示
                dropDownForm.Location = new Point(formX, formY);
                dropDownForm.Show();
            }

            // dataGridViewの列幅の計算
            int totalWidth = 0;
            foreach (DataGridViewColumn column in dataGridView.Columns)
            {
                totalWidth += column.Width;
            }
            dropDownForm.Width = totalWidth + 20;
        }   // EOF ShowDropDownForm

        /// --------------------------------------------------------------------------------
        /// <summary>
        /// コンボボックスの行選択時、イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// --------------------------------------------------------------------------------
        private void DataGridView_CellClick(object sender, DataGridViewCellEventArgs e)
        {
            if (e.RowIndex >= 0)
            {
                if (dataGridView.Rows[e.RowIndex].Cells[e.ColumnIndex].Value != null)
                {
                    // テキストボックスに選択値のセット
                    //textBox.Text = dataGridView.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString();
                    textBox.Text = dataGridView.Rows[e.RowIndex].Cells[0].Value.ToString();

                    sNAME = dataGridView.Rows[e.RowIndex].Cells[1].Value.ToString();
                }
                dropDownForm.Hide();
            }
        }
    }   // EOF Class
}   // EOF namespace

Form(コンボボックスを配置した)のソース

        /// --------------------------------------------------------------------------------
        /// <summary>
        /// フォーム読み込み時、イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// --------------------------------------------------------------------------------
        private void FrmMain_Load(object sender, EventArgs e)
        {
            // コンボボックスのデータの準備
            List<DataCombo> items = new List<DataCombo>();
            for (int i = 0; i < 100; i++)
            {
                DataCombo row = new DataCombo
                {
                    Id = i.ToString("D4"),
                    Name = "Item " + i.ToString(),
                    Description = "Description " + i.ToString()
                };
                items.Add(row);
            };
            customCombo1.SetData(items);

            boLoading = false;
        }

        /// --------------------------------------------------------------------------------
        /// <summary>
        /// コンボボックスで、行選択時イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// --------------------------------------------------------------------------------
        private void customCombo1_TextBoxTextChanged(object sender, EventArgs e)
        {
            // コンボボックスで、選択された行の、2列目の取得
            lblComboName.Text = customCombo1.sNAME;
        }

開発環境の選択について

■開発言語を学ぶ以前の問題として、開発環境として
一体どれを選べば良い? 🤔という疑問があります。

・参考に2024年02月現在、私が業務系アプリ開発で使用している主な開発環境を紹介させて頂きます。
(私が昭和生まれの Microsoft信者であることを先に言っておきます!)
 多分、下記の環境は現在のメジャーかと思います。
 細かい説明は省略しますが、目的によって、それぞれ使い分けています。

①Windows Desktop用: <–デスクトップで、レスポンス重視
 •C# .Net framework (Win Forms) <–従来のシステムサポート
 •C# .Net6(Win Forms)
②Azure Web App用: <–WEBで配布が楽
 •Asp.Net 4.8(Web Forms) <–従来のシステムサポート
 •Asp.Net Core Api(C#) サーバー側 (↓のBack-End 親
 •Blazor Web-Assembly(C#.Net)クライアント側 (↑のFront-End 子

  •Blazor Server(C#) <– (Back-End/Front-Endに分けたくない場合)
③Azure Console App用: 主に夜間バッチ処理
 •C# .Net6
④Azure SQL Database(データベース)
 •現在は、Azure SQLメイン。(その他: Oracle<-ユーザーにより)

↑言語は、現在C#.Net5~C#.Net8へ移行中。
(C#.Net8に手を出すのはまだ、少し早いかも)
.NetFramework4.8は、Windows OSにあらかじめインストールされており、まだまだ現役。ただ今後は、.Net( 旧.Net Core)が最終的に継続サポートされる。

■プログラミング歴30年続けてきて、思う事。
・開発の言語仕様、コンポーネントも毎月のようにバージョンアップしています。サポート切れにならないように、常に新しいものを試して次に繋げていかないと行けません。
・ また、⏳ 時間には限りがあるので、選んで学ばないといけません!
何でも手を出すべきではないです。
今までやってきたことを継続しながら、次に学ぶ物を決めるのが無難。
・そして大事なのは、トライエラーの繰り返しです! 😉

バーコード・システム / WEBアプリ版 v.1.0.8

初期バージョンが「バーコードスキャン」+「数量入力」だけだったので、今回は、収集したデータ一覧の画面を追加しました。

最初の「QRスキャン」画面の「読み取り終了」をクリックすると、作成されたデータの一覧画面を表示します。

この先は、業務アプリなら、データベースに登録という流れだと思いますが、WEB公開しているので、指定したメールアドレスに、CSV形式でメール送信という流れにしようかと考えています。

 

https://mobilesta-wasm01.azurewebsites.net

バーコード照合 / WEBアプリ版

先回、作成したQRコードスキャンのテストプログラムに少し手を加えて、2つのバーコードを照合するアプリを作成しました。

2つが一致すれば、OKサウンド不一致ならNGサウンドを鳴らします。棚入れ時の照合や、検品などに使えるかと思います。

↓↓↓↓↓こちらのリンクからアプリを起動できます。↓↓↓↓↓

https://mobilesta-wasm01.azurewebsites.net/barcheck

バーコード・システム / WEBアプリ版

WEBアプリで、バーコードが読み取れるのか試してみました。作成したアプリを公開します。動作のお試しレベルですが・・・。「QRスキャン」となっていますが、QRとJANコードが読み取れます。

結果は、問題なく読み取れます。ただ、スマートフォンによって画面のサイズが違うのと、カメラ性能の違いなどで、操作感が違うなあと感じましたが、問題になるレベルではないです。

また、当然ですが専用のハンディーターミナルに比べると、明らかに読み取り性能や操作性は悪いです。それでも、慣れは必要かと思われますが、検品や棚卸作業レベルなら、使えるレベルだと思います。

↓↓↓↓↓こちらのリンクからアプリを起動できます。↓↓↓↓↓

https://mobilesta-wasm01.azurewebsites.net

WebAssemblyでの動作は、ネットワークに繋がっていなくても問題ないとの事ですが、実際にネットワークを切断して試してみましたが、少なくとも最初の起動時は、繋がっていないとダメなようです。wwwroot直下のリソースファイルを見に行くのか、最新バージョンをチェックしているのだと思います。

今まで、モバイルアプリ開発だと、Visual Studio + UWPや、Android Studio (Java)を使って開発していたのですが、普段使い慣れていないので結構大変でした。また、iPhone向けには、Mac Bookを購入してSwingを覚えないといけない等、なかなか手が出せない状況だったので、WEBアプリである程度の事ができるようになったのは、非常に有難いです。今までプログラマーを続けてきて、やっとこのような時代になったんだなあと、感慨深いものがあります。

Web Api サーバー側サンプル(Asp.Net Core)

WEBシステムとの連携では、よくWEB-APIを使用しますが、そのWEB-APIのサーバー側処理のサンプルを作ってみました。動作環境としては、Azure Appサービスを使っています。(無料でいろいろできるので)

サンプルは、クライアントから、postリクエストを送信します。エンドポイント(url) = https://xxxxxxxx/api/linewebhook として作成しました。ClsPostDataクラスは、リクエストを受取る為の構造体です。

初めてWEB-APIを見る人は、良くわからないと思いますが、[HttpPost]に続く関数が、POSTリクエストの受信時に発生するイベントです。

Class名 = LineWebHookController の場合、エンドポイント(url)は、xxx/api/linewebhook となります。(そういう決まりになっている為)

※【注意】下記のサンプルでは、リクエスト元の認証の処理が書かれていません。実際に使用する際には、リクエストのヘッダー情報にシークレットKEYをセットして認証するなどの処理が必要です。

using Microsoft.AspNetCore.Mvc;
using System.Net.Mail; 
namespace WebApiPost01.Controllers 
{
	[Route("api/[controller]")] 
	[ApiController] 
	public class LineWebHookController : ControllerBase 
	{ 
		[HttpPost] 
		public IActionResult Post([FromBody] ClsPostData postData) 
		{ 
			// POSTリクエストデータ有無チェック 
			if (postData == null) 
			{ 
				return BadRequest("Invalid request data.");
			} 
			// メール送信 (POSTリクエストを受信した時に実行する処理)			
			Fn_SendMail(postData.Title, postData.Message); 
			// 戻り値(クライアント側に返す値) 
			return Ok(new { Message =
					$"受信: タイトル = {postData.Title}," + 
					$"内容 = {postData.Message}"
			}); 
		} 
	} // EOF POST

	// POSTリクエスト取得用クラス 
	public class ClsPostData 
	{ 
		public string Title { get; set; } 
		public string Message { get; set; } 
		// ... 他のフィールドも追加できます 
	} 
}

Asp.Net Coreには、クライアントからのPOSTリクエストをテストする、swaggerというツールが標準でついていますが、あえてPOSTMANでテストしました。
送信データは、通常JSON形式で渡す決まりになっています。このJSONデータをサーバー側のClsPostDataクラスで受け取ります。

配列あれこれ(C#)

今回は、プログラムの基本の配列について、いくつかサンプルを作成してみました。(言語は、c#を想定しています。)

[1] 最初のパターンは、文字列の配列宣言と同時にデータもセットする方法です。(宣言と同時に配列数が決まります。)

// ■ 
// ■
// ■ パターン①
// 文字列配列、宣言&初期化
string[] sArry = new string[] { "Jan", "Feb", "Mar", "Apr",
        "May" ,"Jun", "Jul","Aug","Sep","Oct","Nov","Dec"};
// 配列のループ(foreachを使った方法)
foreach (string str in sArry)
{   // 出力
    Debug.WriteLine(str);
}
 [2] 次のパターンは、配列を宣言してから、追加してゆく方法です。こちらも基本的に配列数が最初に決まっていますが、後から増減させることは可能です。
 
// ■ 
// ■
// ■ パターン②
string[] sArry2 = new string[12];
sArry2[0] = "Jan";
sArry2[1] = "Feb";
sArry2[2] = "Mar";
sArry2[3] = "Apr";
sArry2[4] = "May";
sArry2[5] = "Jun";
sArry2[6] = "Jul";
sArry2[7] = "Aug";
sArry2[8] = "Sep";
sArry2[9] = "Oct";
sArry2[10] = "Nov";
sArry2[11] = "Dec";
// 配列のループ(forを使った方法)
for (int i = 0; i < 12; i++)
{   // 出力
    Debug.WriteLine(sArry2[i]);
}

[3] 3つ目のパターンは、List<> を使用した方法です。この方法が汎用性が高く、<string>の中には、string(文字列)だけでなく、個別にclassで宣言した型(構造体)を指定できるので、配列をデータテーブルのように使うことができます。

業務系アプリで、データを扱う場合は必須な方法かと思います。

// ■ 
// ■
// ■ パターン③
List sList<string> = new List<string>();
sList.Add("Jan");
sList.Add("Feb");
sList.Add("Mar");
sList.Add("Apr");
sList.Add("May");
sList.Add("Jun");
sList.Add("Jul");
sList.Add("Aug");
sList.Add("Sep");
sList.Add("Oct");
sList.Add("Nov");
sList.Add("Dec");
// 配列のループ(foreachを使った方法)
foreach (string str in sArry)
{   // 出力
    Debug.WriteLine(str);
}

[4] 最後のパターンは、System.Data.DataTable を使った方法です。業務系のアプリの場合は、必須かと思います。

名前の通りにメモリー上にテーブルを作成して使用します。型宣言が面倒に思われるかと思いますが、データベースとのやり取りがあるシステムなら、親和性が高くデータのやり取りがとてもスムーズにできます。

// ■
// ■
// ■ パターン④
DataTable dt = new DataTable();
dt.Columns.Add("No", typeof(int));      // 列(項目)を追加
dt.Columns.Add("月",typeof(string));    // 列(項目)を追加
for (int i = 0; i < 12; i++)
{   // データセット
    DataRow row = dt.NewRow();  // テーブルの新しい行宣言
    row["No"] = i;
    row["月"] = i.ToString("D2") + "月";  // 行の項目に値セット
    // テーブルに行を追加
    dt.Rows.Add(row);
}
// 配列のループ(foreachを使った方法)
foreach (DataRow data in dt.Rows)
{   // 出力
    Debug.WriteLine(
        data["No"].ToString(),
        data["月"].ToString()
        );
}