using HotelPms.Client.Blazor.Pages.UseDetail; using HotelPms.Client.Blazor.Util; using HotelPms.Share.IO; using HotelPms.Share.Util; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; using System.Collections.Concurrent; using System.Reflection.Metadata; using System.Text; namespace HotelPms.Client.Blazor.ViewModel { /// /// ViewModelとして使って、 /// Windows Fromスタイルの入力チェック方式 /// public abstract class ValidModel : IDisposable { #region ★★★★★ Declartions ★★★★★ public delegate void ValidEventHandler(ValidField sender, ValidEventArgs e); public delegate Task ValidResultEventHandler(ValidField sender, ValidEventArgs e); public event ValidResultEventHandler BeforeAutoNextFocus; public event ValidEventHandler AfterAutoNextFocus; public event ValidEventHandler AfterEnter; public event ValidEventHandler AfterLeave; public event ValidResultEventHandler ValueChanged; public event ValidEventHandler ShowList; public event ValidResultEventHandler BusinessValid; public static string Chr_Num = "0123456789"; public static string Chr_Alpha_S = "abcdefghijklmnopqrstuvwxyz "; public static string Chr_Alpha_C = "ABCDEFGHIJKLMNOPQRSTUVWXYZ "; public static string Chr_Kana = "ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚ "; public static string Chr_Alpha = Chr_Alpha_S + Chr_Alpha_C; public static string Chr_NumAlpha = Chr_Num + Chr_Alpha; public static string Chr_ANK = Chr_NumAlpha + Chr_Kana; #endregion #region ★★★★★ Property ★★★★★ public IJSRuntime JSRuntime { get; set; } public List Fields { get; set; } = new List(); private Dictionary dict = new Dictionary(); /// /// UIを更新するため、親画面よりCallBackを受ける /// public Action Refresh { get; set; } /// /// Enterキーを押したかどうか /// public bool EnterPush { get; set; } /// /// Upキー押したかどうか /// public bool ArrowUpPush { get; set; } public bool KeyDownPreventDefault { get; set; } public bool KeyPressPreventDefault { get; set; } /// /// 0.新規 1.編集 2.削除 /// public int EditMode { get; set; } /// /// 全体のエラー /// public string ErrText { get; set; } /// /// 一覧表示の検索キー対象項目一覧 /// public List FilterFields { get; set; } = new List(); /// /// 一覧表示の順番対象項目一覧 /// public SortedList OrderFields { get; set; } = new SortedList(); /// /// Guid /// public string ID { get; set; } = string.Empty; /* ■ロック追加前 Leave⇒Start Leave⇒Row:1 Col:0 enterPush:False, orgText:, text:1 【10:07:06 046】SendAsync begin:10:07:06 046 【10:07:06 047】SendAsync Url⇒/HotelPms.Data.GrpcTableCore/GetData Enter⇒Row:0 Col:0 Text:1 【10:07:06 095】SendAsync End:10:07:06 095 該当部屋タイプが既に存在します。 Leave⇒End ■追加後 Leave⇒Start Leave⇒Row:1 Col:0 enterPush:False, text:1 【10:26:02 811】SendAsync begin:10:26:02 811 【10:26:02 811】SendAsync Url⇒/HotelPms.Data.GrpcTableCore/GetData 【10:26:02 860】SendAsync End:10:26:02 860 該当部屋タイプが既に存在します。 Leave⇒End Enter⇒Row:0 Col:0 Semaphore (int initialCount, int maximumCount); initialCount代表还分配几个线程,比如是1,那就是还能允许一个线程继续跑锁起来的代码 maximumCount代表最大允许数,比如是1,那就是进去1个线程,就会锁起来 */ private System.Threading.SemaphoreSlim slimlock = new SemaphoreSlim(1, 1); #endregion #region ★★★★★ Class Event ★★★★★ public ValidModel(IJSRuntime js) { JSRuntime = js; ID = Guid.NewGuid().ToString(); } public void Dispose() { } #endregion #region ★★★★★ Control Event ★★★★★ /// /// ①フォーカス入る /// ※BlazorのBugでEnterのパラメータ変数できない /// /// public async Task Enter(int index, FocusEventArgs e) { await slimlock.WaitAsync(); EnvironmentSetting.Debug($"Enter:{index}⇒OrgText={Fields[index].OrgText},Text={Fields[index].Text}"); Fields[index].OrgText = Fields[index].Text; if ((Fields[index].ShowStyle & SystemEnum.EShowStyle.ThousandSeparator) == SystemEnum.EShowStyle.ThousandSeparator && Fields[index].Text.Length > 0) { Fields[index].Text = Fields[index].Text.Replace(",", string.Empty); //コントロールの値連動する //await Fields[index].Ref.SetInputValue(JSRuntime, Fields[index].Text); } if (AfterEnter != null) { AfterEnter(Fields[index], new ValidEventArgs()); } if (Refresh != null) { Refresh(); } //UI更新(binding値変更通知) slimlock.Release(); } /// /// ※IMEでもここに通る /// 例:「おぎ」を打つ時、[O]⇒e.Key="Proess"、e.KeyCode="KeyO"、e.Type="KeyDown" /// ②Enter⇒KeyDown⇒KeyPress⇒Leave /// /// /// /// public async Task KeyDown(int index, KeyboardEventArgs e) { if (e.Key == "ArrowUp") { try { EnvironmentSetting.Debug($"ArrowUp:{index}"); if (await SetAutoNextFocus(index, false)) { //BlazorのBugでEnterのパラメータ変数できない! //Uncaught Error: System.ArgumentException: There is no event handler associated with this event. EventId: '290'. (Parameter 'eventHandlerId') //mkArtakMSFT modified the milestones: Next sprint planning, 6.0-preview4 on 20 Mar ← Net6.0-preview4対応だそう KeyDownPreventDefault = true; ArrowUpPush = true; } else { KeyDownPreventDefault = false; ArrowUpPush = false; } } catch { } finally { if (Refresh != null) { Refresh(); } //UI更新(binding値変更通知) } } else if (e.Key == "End") { if (ShowList != null) { ShowList(Fields[index], new ValidEventArgs()); } } else { KeyDownPreventDefault = false; ArrowUpPush = false; } } /// /// ※注意:IMEモードではここに通らない /// e.Codeだと、二つEnterが違う /// ※※注意:ここで@bind-Valueを変更しても、直ぐにinputへ反映しない /// 必ずjs側で強制的にtextをセットする /// 整合性を保るため、@bind-Valueも同じでセットする /// /// /// /// public async Task KeyPress(int index, KeyboardEventArgs e) { if (e.Key == "Enter") { try { //※その時、Fields[index].Textの値まだ変っていない!!!!! string text = await Fields[index].Ref.GetInputValue(JSRuntime); EnvironmentSetting.Debug($"Return:{index}⇒{text}"); //値変更したら、イベント発生 if (await IsValueChanged(index, text, true)) { //通常チェックを行う if (!await IsCorrectText(index, text, true)) { return; } //業務チェックを行う ValidEventArgs vea = new ValidEventArgs() { Text = text, EnterPush = true }; bool bvRet = await BusinessValid(Fields[index], vea); EnvironmentSetting.Debug($"BusinessValid結果(Enter):{bvRet},Error:{Fields[index].Error},index={index}"); KeyPressPreventDefault = true; //イベント中止 EnterPush = true; //Enterを押した知らせ if (bvRet) { SetOrgText(index, text); } else { return; } //BusinessValid結果:OK⇒OrgTextを反映する NG⇒前へ進まない } else { //必須判断:初期表示時:OrgTextは空白のため、値変更しないまま if (text.Trim().Length == 0 && Fields[index].Required) { Fields[index].Error = true; Fields[index].ErrorText = "内容を入力してください。"; return; } } if (!await SetAutoNextFocus(index, true)) { return; } } catch { } finally { if (Refresh != null) { Refresh(); } //UI更新(binding値変更通知) } } else { if (TextKeyPress(index, e.Key)) { KeyPressPreventDefault = false; } else { KeyPressPreventDefault = true; //入力禁止(※:IMEモードで入力したものをここでは通らないため、防げない) } EnterPush = false; } } /// /// そのタイミングbind反映したはず /// /// /// //private async Task LeaveMud(int index) public async Task Leave(int index, FocusEventArgs e) { string text = string.Empty; try { await slimlock.WaitAsync(); EnvironmentSetting.Debug($"Leave:{index}⇒OrgText={Fields[index].OrgText},Text={Fields[index].Text}"); text = Fields[index].Text; if (EnterPush) { //Enterキーを押した時にも既にチェック済 EnterPush = false; } else { //値変更したら、イベント発生 if (!await IsValueChanged(index, text, false)) { //必須判断:初期表示時:OrgTextは空白のため、値変更しないまま Fields[index].Error = false; Fields[index].ErrorText = string.Empty; return; } //通常チェックを行う if (await IsCorrectText(index, text, false)) { //業務チェックを行う ValidEventArgs vea = new ValidEventArgs() { Text = text }; bool bvRet = await BusinessValid(Fields[index], vea); EnvironmentSetting.Debug($"BusinessValid結果:{bvRet},Error:{Fields[index].Error},index={index}"); if (!bvRet) { Fields[index].Error = true; } } } if (Fields[index].Error) { EnvironmentSetting.Debug($"元値に復元する↓:Error={Fields[index].Error},index={index}"); //エラーの場合、元値に復元する //await Fields[index].Ref.SetInputValue(JSRuntime, Fields[index].OrgText); Fields[index].Text = Fields[index].OrgText; Fields[index].Error = false; Fields[index].ErrorText = string.Empty; EnvironmentSetting.Debug($"元値に復元する↑:Error={Fields[index].Error},ErrorText={Fields[index].ErrorText},Text={Fields[index].Text},index={index}"); return; } SetOrgText(index, text); } catch (Exception ex) { OperationLog.Instance.WriteLog(ex.Message); } finally { if ((Fields[index].ShowStyle & SystemEnum.EShowStyle.ThousandSeparator) == SystemEnum.EShowStyle.ThousandSeparator && text.Length > 0) { text = CConvert.ToThousandSeparator(CConvert.ToDecimal(text), Fields[index].ThousandFormat); //コントロールの値連動する (小数点!!!!) await SetText(index, text, false); } if (AfterLeave != null) { AfterLeave(Fields[index], new ValidEventArgs()); } if (ArrowUpPush) { ArrowUpPush = false; } if (Refresh != null) { Refresh(); } //UI更新(binding値変更通知) slimlock.Release(); } } #endregion #region ★★★★★ Private Function ★★★★★ private async Task SetAutoNextFocus(int index, bool isEnter) { try { if (BeforeAutoNextFocus != null) { ValidEventArgs args = new ValidEventArgs() { EnterPush = isEnter }; if (!await BeforeAutoNextFocus(Fields[index], args)) { return false; } } int hitIdx = -1; if (isEnter) { for (int i = index + 1; i < Fields.Count; i++) { if (!Fields[i].Disabled) { hitIdx = i; break; } } if (hitIdx == -1) { return false; } } else { if (index == 0) { return true; } for (int i = index - 1; i >= 0; i--) { if (!Fields[i].Disabled) { hitIdx = i; break; } } if (hitIdx == -1) { return false; } } await Fields[hitIdx].Ref.FocusAsync(); return true; } catch { return false; } finally { if (AfterAutoNextFocus != null) { AfterAutoNextFocus(Fields[index], new ValidEventArgs() { EnterPush = isEnter }); } } } /// /// ※注意:IME起動時、ここに通らない /// /// /// /// private bool TextKeyPress(int index, string Key) { try { SystemEnum.EInputChar inputType = Fields[index].InputChar; if (inputType == SystemEnum.EInputChar.None) { return true; } char hKeyAscii = Key[0]; if (hKeyAscii == 8) { return true; } else if (hKeyAscii == "'"[0]) { hKeyAscii = (char)0; return false; } else if (hKeyAscii == 13) { //hKeyAscii = (char)0; return false; } if ((inputType & SystemEnum.EInputChar.Num) == SystemEnum.EInputChar.Num) { if ('0' <= hKeyAscii && hKeyAscii <= '9') { return true; } } if ((inputType & SystemEnum.EInputChar.Dot) == SystemEnum.EInputChar.Dot) { if (hKeyAscii == '.') { return true; } } if ((inputType & SystemEnum.EInputChar.Slash) == SystemEnum.EInputChar.Slash) { if (hKeyAscii == '/') { return true; } } if ((inputType & SystemEnum.EInputChar.Colon) == SystemEnum.EInputChar.Colon) { if (hKeyAscii == ':') { return true; } } if ((inputType & SystemEnum.EInputChar.Subtract) == SystemEnum.EInputChar.Subtract) { if (hKeyAscii == '-') { return true; } } if ((inputType & SystemEnum.EInputChar.Comma) == SystemEnum.EInputChar.Comma) { if (hKeyAscii == ',') { return true; } } if ((inputType & SystemEnum.EInputChar.Space) == SystemEnum.EInputChar.Space) { if (hKeyAscii == ' ') { return true; } } if ((inputType & SystemEnum.EInputChar.Alpha_C) == SystemEnum.EInputChar.Alpha_C) { if ('A' <= hKeyAscii && hKeyAscii <= 'Z') { return true; } } if ((inputType & SystemEnum.EInputChar.Alpha_S) == SystemEnum.EInputChar.Alpha_S) { if ('a' <= hKeyAscii && hKeyAscii <= 'z') { return true; } } if ((inputType & SystemEnum.EInputChar.Kana) == SystemEnum.EInputChar.Kana) { if ('ヲ' <= hKeyAscii && hKeyAscii <= '゚') { return true; } } if ((inputType & SystemEnum.EInputChar.Half) == SystemEnum.EInputChar.Half) { if (hKeyAscii >= 0 && hKeyAscii <= 255) { return true; } } if ((inputType & SystemEnum.EInputChar.Full) == SystemEnum.EInputChar.Full) { if (hKeyAscii < 0 || hKeyAscii > 255) { return true; } } return false; } catch { return false; } } private async Task IsValueChanged(int index, string inputText, bool isEnter) { string newInputValue = string.Empty; if ((Fields[index].ShowStyle & SystemEnum.EShowStyle.ThousandSeparator) == SystemEnum.EShowStyle.ThousandSeparator && inputText.Length > 0) { newInputValue = CConvert.ToThousandSeparator(CConvert.ToDecimal(inputText), Fields[index].ThousandFormat); //※OrgTextはカンマ区切りのまま } else { newInputValue = inputText; } EnvironmentSetting.Debug($"CheckValueChanged:{index}⇒inputText={newInputValue},OrgText={Fields[index].OrgText}"); if (newInputValue.Equals(Fields[index].OrgText)) { return false; } //値変更したかどうか if (ValueChanged != null) { //string saveText = inputText; ValidEventArgs vce = new ValidEventArgs() { Text = inputText, EnterPush = isEnter }; if (!await ValueChanged(Fields[index], vce)) { //元の値に戻す if (vce.ResetOrgValue) { await SetText(index, Fields[index].OrgText, isEnter); } return false; //外から強制的に値変らないようにする } //await SetText(index, saveText, isEnter); //変更された恐れがあるため } return true; } private async Task SetText(int index, string text, bool updateRef) { Fields[index].Text = text; if (updateRef) { await Fields[index].Ref.SetInputValue(JSRuntime, text); } } private void SetOrgText(int index, string inputText) { if ((Fields[index].ShowStyle & SystemEnum.EShowStyle.ThousandSeparator) == SystemEnum.EShowStyle.ThousandSeparator) { Fields[index].OrgText = CConvert.ToThousandSeparator(CConvert.ToDecimal(inputText), Fields[index].ThousandFormat); //※OrgTextはカンマ区切りのまま } else { Fields[index].OrgText = inputText; } } private bool IsCorrectValue(int index, string inputText) { string wsChkStr = ""; SystemEnum.EInputChar wlErrFlg = 0; try { string text = (Fields[index].ShowStyle & SystemEnum.EShowStyle.ThousandSeparator) == SystemEnum.EShowStyle.ThousandSeparator ? inputText.Replace(",", string.Empty) : inputText; SystemEnum.EInputChar inputType = Fields[index].InputChar; //半角チェック if ((inputType & SystemEnum.EInputChar.Half) == SystemEnum.EInputChar.Half && !CConvert.IsHalf(text)) { Fields[index].Error = true; Fields[index].ErrorText = "半角入力してください。"; return false; } //全角チェック if ((inputType & SystemEnum.EInputChar.Full) == SystemEnum.EInputChar.Full && !CConvert.IsFull(text)) { Fields[index].Error = true; Fields[index].ErrorText = "全角入力してください。"; return false; } //数字 if ((inputType & SystemEnum.EInputChar.Num) == SystemEnum.EInputChar.Num) { wsChkStr = wsChkStr + Chr_Num; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Num; } //. if ((inputType & SystemEnum.EInputChar.Dot) == SystemEnum.EInputChar.Dot) { wsChkStr = wsChkStr + "."; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Dot; } //[/] if ((inputType & SystemEnum.EInputChar.Slash) == SystemEnum.EInputChar.Slash) { wsChkStr = wsChkStr + "/"; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Slash; } //Space if ((inputType & SystemEnum.EInputChar.Space) == SystemEnum.EInputChar.Space) { wsChkStr = wsChkStr + " "; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Space; } //- if ((inputType & SystemEnum.EInputChar.Subtract) == SystemEnum.EInputChar.Subtract) { wsChkStr = wsChkStr + "-"; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Subtract; } //: if ((inputType & SystemEnum.EInputChar.Colon) == SystemEnum.EInputChar.Colon) { wsChkStr = wsChkStr + ":"; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Colon; } //, if ((inputType & SystemEnum.EInputChar.Comma) == SystemEnum.EInputChar.Comma) { wsChkStr = wsChkStr + ","; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Colon; } //小英 if ((inputType & SystemEnum.EInputChar.Alpha_C) == SystemEnum.EInputChar.Alpha_C) { wsChkStr = wsChkStr + Chr_Alpha_C; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Alpha_C; } //大英 if ((inputType & SystemEnum.EInputChar.Alpha_S) == SystemEnum.EInputChar.Alpha_S) { wsChkStr = wsChkStr + Chr_Alpha_S; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Alpha_S; } //カナ if ((inputType & SystemEnum.EInputChar.Kana) == SystemEnum.EInputChar.Kana) { wsChkStr = wsChkStr + Chr_Kana; wlErrFlg = wlErrFlg | SystemEnum.EInputChar.Kana; } if (wsChkStr.Length > 0) { if (CConvert.IsInOwnStr(text, wsChkStr) == false) { Fields[index].Error = true; if (wlErrFlg == SystemEnum.EInputChar.Num) { Fields[index].ErrorText = "数字を入力してください。"; } else if (wlErrFlg == SystemEnum.EInputChar.Kana) { Fields[index].ErrorText = "半角カナを入力してください。"; } else if (wlErrFlg == SystemEnum.EInputChar.Alpha_S) { Fields[index].ErrorText = "小英文字を入力してください。"; } else if (wlErrFlg == SystemEnum.EInputChar.Alpha_C) { Fields[index].ErrorText = "大英文字を入力してください。"; } else { Fields[index].ErrorText = "正しい値を入力してください。"; } return false; } } return true; } catch (Exception ex) { OperationLog.Instance.WriteLog($"IsCorrectValue:{ex.Message}"); return false; } } private async Task IsCorrectText(int index, string inputText, bool isEnter) { //必須判断 if (inputText.Trim().Length == 0 && Fields[index].Required) { Fields[index].Error = true; Fields[index].ErrorText = "内容を入力してください。"; return false; } //入力文字判断 if (!IsCorrectValue(index, inputText)) { return false; } //日付判断 if (Fields[index].InputStyle == SystemEnum.EInputStyle.Date) { if (!CConvert.IsDate(CConvert.ToInt(inputText.Replace("/", string.Empty).Replace("-", string.Empty)).ToString().PadLeft(8, '0'))) { Fields[index].Error = true; Fields[index].ErrorText = "正しい日付を入力してください。"; return false; } } //範囲判断 if (Fields[index].Range.Length > 0) { if (CConvert.IsOutOfRange(inputText, Fields[index].Range)) { Fields[index].Error = true; Fields[index].ErrorText = "入力範囲以外です。"; return false; } } //「0」詰合 if (inputText.Length > 0) { if ((Fields[index].ShowStyle & SystemEnum.EShowStyle.ZeroPad) == SystemEnum.EShowStyle.ZeroPad) { string text = inputText.PadLeft(Fields[index].MaxLenth, '0'); await SetText(index, text, isEnter); } } return true; } #endregion #region ★★★★★ Public Function ★★★★★ public void Add(ValidField field) { field.Index = Fields.Count; Fields.Add(field); dict.Add(field.Name, field); if (field.GridFilter) { FilterFields.Add(field.Name); } if (field.GridOrder > 0) { OrderFields.Add(field.GridOrder, field.Name); } } public void RemoveAt(int index) { string key = Fields[index].Name; if (Fields[index].GridFilter) { FilterFields.Remove(key); } if (Fields[index].GridOrder > 0) { OrderFields.Remove(Fields[index].GridOrder); } dict.Remove(key); Fields.RemoveAt(index); for (int i = index; i < Fields.Count; i++) { Fields[i].Index -= 1; } } public void Remove(ValidField field) { RemoveAt(field.Index); } public void Remove(string key) { ValidField field = dict[key]; RemoveAt(field.Index); } public ValidField GetField(string key) { return dict[key]; } public void SetField(string key, string value) { dict[key].Text = value; } public void Clear() { ErrText = string.Empty; EnterPush = false; ArrowUpPush = false; KeyDownPreventDefault = false; KeyPressPreventDefault = false; foreach (ValidField field in Fields) { field.Clear(); } } public async Task IsValidAll() { try { ErrText = string.Empty; foreach (ValidField field in Fields) { string text = await field.Ref.GetInputValue(JSRuntime); //この時点ValidField.Textまだ反映していない if (!await IsCorrectText(field.Index, text, false)) { ErrText = field.ErrorText; return false; } if (BusinessValid != null) { ValidEventArgs e = new ValidEventArgs() { Text = text, IsAll = true }; if (!await BusinessValid(field, e)) { ErrText = field.ErrorText; return false; } } } return true; } catch { return false; } finally { if (Refresh != null) { Refresh(); } } } /// /// マスタメンテナンス画面のフィルター条件文返す /// /// /// public string GetFilterSql(string key) { if (string.IsNullOrEmpty(key) || FilterFields.Count == 0) { return string.Empty; } StringBuilder sql = new StringBuilder(); sql.Append("("); for (int i = 0; i < FilterFields.Count; i++) { if (i > 0) { sql.Append(" OR "); } sql.Append($"{FilterFields[i]} LIKE N''%{CConvert.ToSql(key)}%''"); } sql.Append(")"); return CConvert.ToBase64(sql.ToString()); } /// /// マスタメンテナンス画面の一覧の順番 /// /// public string GetOrderSql() { if (OrderFields.Count == 0) { return string.Empty; } StringBuilder sql = new StringBuilder(); bool isFirst = true; foreach (KeyValuePair item in OrderFields) { if (isFirst) { isFirst = false; } else { sql.Append(","); } sql.Append(item.Value); } return sql.ToString(); } #endregion } }