using HotelPms.Client.Blazor.Pages.UseDetail; using HotelPms.Client.Blazor.Services; using HotelPms.Client.Blazor.Util; using HotelPms.Share.IO; using HotelPms.Share.Util; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; using System.Collections.Concurrent; using System.Reflection.Metadata; using System.Text; namespace HotelPms.Client.Blazor.ViewModel { /// /// ViewModelExとして使って、 /// Windows Fromスタイルの入力チェック方式 /// C# Call jsは遅いため、逆にjsよりCallBackと使用する /// ※@bind-value使わない /// public abstract class ValidModelEx : 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 ValidResultEventHandler 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 ★★★★★ /// /// ID毎のインスタンス格納 /// public static ConcurrentDictionary Storage { get; set; } = new ConcurrentDictionary(); public IJSRuntime JSRuntime { get; set; } public List Fields { get; set; } = new List(); private Dictionary dict = new Dictionary(); public Action ErrRefresh { get; set; } /// /// Enterキーを押したかどうか /// public bool EnterPush { get; set; } /// /// Upキー押したかどうか /// public bool ArrowUpPush { 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; public bool DisableKeyboard { get; set; } = false; #endregion #region ★★★★★ Class Event ★★★★★ public ValidModelEx(IJSRuntime js) { JSRuntime = js; ID = Guid.NewGuid().ToString(); Storage[ID] = this; } public void Dispose() { Storage.TryRemove(ID, out ValidModelEx model); } #endregion #region ★★★★★ Control Event ★★★★★ /// /// ①フォーカス入る /// ※BlazorのBugでEnterのパラメータ変数できない /// /// public void Enter(int index, JsInputCoreEventArgs e) { EnvironmentSetting.Debug($"Enter:{index}⇒OrgText={e.OrgText},Text={e.Text}"); Fields[index].OrgText = e.OrgText; if ((Fields[index].ShowStyle & SystemEnum.EShowStyle.ThousandSeparator) == SystemEnum.EShowStyle.ThousandSeparator && Fields[index].Text.Length > 0) { Fields[index].Text = Fields[index].Text.Replace(",", string.Empty); //コントロールの値連動する } e.ResultNo = 0; if (AfterEnter != null) { AfterEnter(Fields[index], new ValidEventArgs()); } } /// /// ※IMEでもここに通る /// 例:「おぎ」を打つ時、[O]⇒e.Key="Proess"、e.KeyCode="KeyO"、e.Type="KeyDown" /// ②Enter⇒KeyDown⇒KeyPress⇒Leave /// /// /// /// public async Task KeyUp(int index, JsInputCoreEventArgs e) { if(DisableKeyboard) { return; } if (e.Key == "ArrowUp") { try { EnvironmentSetting.Debug($"KeyUp⇒ArrowUp:{index}"); if (!await SetAutoNextFocus(index, false, e)) { return; } e.ResultNo = 0; } catch { } finally { } } else if (e.Key == "End") { EnvironmentSetting.Debug($"KeyUp⇒End:{index}"); if (ShowList != null) { DisableKeyboard = true; ValidEventArgs eventArgs = new(); bool ret = await ShowList(Fields[index], eventArgs); DisableKeyboard = false; if (!eventArgs.Cancel) { await SetAutoNextFocus(index, true, e); } } e.Text = Fields[index].Text; e.ResultNo = 0; } else if (e.Key == "Enter") { await KeyEnter(index, e); } } /// /// ※注意:IMEモードではここに通らない /// e.Codeだと、二つEnterが違う /// ※※注意:ここで@bind-Valueを変更しても、直ぐにinputへ反映しない /// 必ずjs側で強制的にtextをセットする /// 整合性を保るため、@bind-Valueも同じでセットする /// /// /// /// public async Task KeyEnter(int index, JsInputCoreEventArgs e) { try { FormatInput(index, e); string text = e.Text; EnvironmentSetting.Debug($"Return:{index}⇒{text}"); //値変更したら、イベント発生 if (await IsValueChanged(index, text, true, e)) { //通常チェックを行う Fields[index].Error = false; Fields[index].ErrorText = string.Empty; if (!IsCorrectText(index, text, true)) { return; } bool bvRet = true; if (BusinessValid != null) { //業務チェックを行う ValidEventArgs vea = new ValidEventArgs() { Text = text, EnterPush = true }; bvRet = await BusinessValid(Fields[index], vea); EnvironmentSetting.Debug($"BusinessValid結果(Enter):{bvRet},Error:{Fields[index].Error},index={index}"); } EnterPush = true; //Enterを押した知らせ if (bvRet) { SetOrgText(index, text); } else { return; } //BusinessValid結果:OK⇒OrgTextを反映する NG⇒前へ進まない Fields[index].Text = text; //入力値確定 e.ResultNo = 0; } else { //必須判断:初期表示時:OrgTextは空白のため、値変更しないまま if (text.Trim().Length == 0 && Fields[index].Required) { Fields[index].Error = true; Fields[index].ErrorText = "内容を入力してください。"; return; } e.ResultNo = 1; } if (!await SetAutoNextFocus(index, true, e)) { e.ResultNo = 9; return; } } catch { } finally { if (ErrRefresh != null && Fields[index].Error) { EnvironmentSetting.Debug($"Error:{Fields[index].Error},ErrorText={Fields[index].ErrorText}"); ErrRefresh(); } } } public void FormatInput(int index, JsInputCoreEventArgs e) { try { if (Fields[index].InputStyle == SystemEnum.EInputStyle.Date) { e.Text = CConvert.ParseDate(e.Text, Fields[index].OrgText); } else if (Fields[index].InputStyle == SystemEnum.EInputStyle.Time) { e.Text = CConvert.ParseTime(e.Text, Fields[index].OrgText); } } catch { } } /// /// そのタイミングbind反映したはず /// /// /// //private async Task LeaveMud(int index) public async Task Leave(int index, JsInputCoreEventArgs e) { string text = string.Empty; bool refresh = false; try { EnvironmentSetting.Debug($"Leave:{index}⇒OrgText={Fields[index].OrgText},Text={Fields[index].Text}"); FormatInput(index, e); text = e.Text; if (EnterPush) { //Enterキーを押した時にも既にチェック済 EnterPush = false; } else { //値変更したら、イベント発生 if (!await IsValueChanged(index, text, false, e)) { //必須判断:初期表示時:OrgTextは空白のため、値変更しないまま Fields[index].Error = false; Fields[index].ErrorText = string.Empty; e.ResultNo = 1; return; } //通常チェックを行う Fields[index].Error = false; Fields[index].ErrorText = string.Empty; if (IsCorrectText(index, text, false)) { if (BusinessValid != null) { //業務チェックを行う 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}"); //エラーの場合、元値に復元する Fields[index].Text = Fields[index].OrgText; e.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}"); refresh = true; return; } Fields[index].Text = text; //確定 e.Text = text; SetOrgText(index, text); e.ResultNo = 0; } 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); //コントロールの値連動する (小数点!!!!) Fields[index].Text = text; e.Text = text; } else if ((Fields[index].ShowStyle & SystemEnum.EShowStyle.ZeroPad) == SystemEnum.EShowStyle.ZeroPad && text.Length > 0) { text = text.PadLeft(Fields[index].MaxLenth, '0'); Fields[index].Text = text; e.Text = text; } if (AfterLeave != null) { AfterLeave(Fields[index], new ValidEventArgs()); } if (ArrowUpPush) { ArrowUpPush = false; } if (refresh && ErrRefresh != null) { ErrRefresh(); } } } #endregion #region ★★★★★ Private Function ★★★★★ private async Task SetAutoNextFocus(int index, bool isEnter, JsInputCoreEventArgs e) { 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; } } e.NextFocus = $"{e.ID}-{hitIdx}"; return true; } catch { return false; } finally { if (AfterAutoNextFocus != null) { AfterAutoNextFocus(Fields[index], new ValidEventArgs() { EnterPush = isEnter }); } } } private async Task IsValueChanged(int index, string inputText, bool isEnter, JsInputCoreEventArgs e) { 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) { ValidEventArgs vce = new ValidEventArgs() { Text = inputText, EnterPush = isEnter }; if (!await ValueChanged(Fields[index], vce)) { //元の値に戻す if (vce.ResetOrgValue) { //※元の値に戻す且つフォーカス移動必要 Fields[index].Text = Fields[index].OrgText; e.Text = Fields[index].OrgText; } return false; //外から強制的に値変らないようにする } } return true; } 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 bool 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; } } 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 SetField(string key, string value, string disp) { dict[key].Text = value; dict[key].DispText = disp; } public void Clear() { ErrText = string.Empty; EnterPush = false; ArrowUpPush = false; foreach (ValidField field in Fields) { field.Clear(); } } public async Task IsValidAll() { try { ErrText = string.Empty; foreach (ValidField field in Fields) { string text = field.Text; if (!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 (ErrText.Length > 0 && ErrRefresh != null) { ErrRefresh(); } } } #endregion } }