WPF用MVVM的解決記錄
版權聲明:本文為博主初學經驗,未經博主允許不得轉載。
一、前言
記錄在學習與制作WPF過程中遇到的解決方案。
焦點的控制,鍵盤事件觸發,輸入框的數字限制,異步處理,隱藏狀態可用狀態,自定義屬性等等...
二、配置
系統環境:win10
開發工具:Visual Studio 2017
開發語言:C#.WPF (MVVM框架)
三、自問自答
1.焦點的控制;
背景:
焦點的使用一般用於輸入框,切換業務功能時,需要焦點定位在指定輸入框位置,便於用戶操作;使用MVVM框架開發后,對於前端控件的焦點控制不便調用,控件綁定的都是偏向於文本內容和事件,不像windowsFrom那般直接調用控件的焦點屬性;
解決方式:
1)自定義屬性;
下面第6點的說明;
2)前端布局所需焦點,后端遍歷;
在Grid的Style樣式里面設置好需要布置焦點的觸發器;
后期每次更改焦點前,都把所有焦點觸發器設置為false,然后在更改指定的焦點為true;
2.1) 前端xaml代碼
<Grid> <Grid.Style> <Style> <Style.Triggers> <DataTrigger Binding="{Binding TxtAFocus}" Value="True"> <Setter Property="FocusManager.FocusedElement"
Value="{Binding ElementName=TxtA}"/> </DataTrigger> <DataTrigger Binding="{Binding TxtBFocus}" Value="True"> <Setter Property="FocusManager.FocusedElement"
Value="{Binding ElementName=TxtB}"/> </DataTrigger> <DataTrigger Binding="{Binding TxtCFocus}" Value="True"> <Setter Property="FocusManager.FocusedElement"
Value="{Binding ElementName=TxtC}"/> </DataTrigger> </Style.Triggers> </Style> </Grid.Style> <StackPanel Margin="10"> <StackPanel Orientation="Horizontal"> <TextBox x:Name="TxtA" Text="" Width="100" Height="30"
Tag="輸入框A..." Style="{StaticResource TxbTrigger}"/> <TextBox x:Name="TxtB" Text="" Width="100" Height="30" Margin="5"
Tag="輸入框B..." Style="{StaticResource TxbTrigger}"/> <TextBox x:Name="TxtC" Text="" Width="100" Height="30"
Tag="輸入框C..." Style="{StaticResource TxbTrigger}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <Button Content="焦點A" Width="80" Height="28" Foreground="White"
Command="{Binding BtnA}" Template="{StaticResource DefaultButton}"/> <Button Content="焦點B" Width="80" Height="28" Foreground="White" Margin="20"
Command="{Binding BtnB}" Template="{StaticResource DefaultButton}"/> <Button Content="焦點C" Width="80" Height="28" Foreground="White"
Command="{Binding BtnC}" Template="{StaticResource DefaultButton}"/> </StackPanel> </StackPanel> </Grid>2.2) 前端xaml的后台cs代碼
DataContext = new FocusAppViewModel();2.3) 后端ViewModel代碼
public class FocusAppViewModel : ViewModelBase { public FocusAppViewModel() { BtnA = new RelayCommand(() => SetFocusA("A")); BtnB = new RelayCommand(() => SetFocusA("B")); BtnC = new RelayCommand(() => SetFocusA("C")); }
public bool TxtAFocus { get => _txtAFocus; set { var valueFocus = value; if (valueFocus) ResetTextFocus(); _txtAFocus = valueFocus; RaisePropertyChanged("TxtAFocus"); } } private bool _txtAFocus; public bool TxtBFocus { get => _txtBFocus; set { var valueFocus = value; if (valueFocus) ResetTextFocus(); _txtBFocus = valueFocus; RaisePropertyChanged("TxtBFocus"); } } private bool _txtBFocus; public bool TxtCFocus { get => _txtCFocus; set { var valueFocus = value; if (valueFocus) ResetTextFocus(); _txtCFocus = valueFocus; RaisePropertyChanged("TxtCFocus"); } } private bool _txtCFocus; public RelayCommand BtnA { get; set; } public RelayCommand BtnB { get; set; } public RelayCommand BtnC { get; set; } private void SetFocusA(string num) { switch (num) { case "A": TxtAFocus = true; break; case "B": TxtBFocus = true; break; case "C": TxtCFocus = true; break; default: TxtAFocus = true; break; } } private void ResetTextFocus() { TxtAFocus = TxtBFocus = TxtCFocus = false; } }2.4) 執行效果
2. 鍵盤事件的觸發;
背景:回車事件,內容更改時發生的事件,鼠標雙擊事件等等綁定事件的操作;
解決方式:
(關於這塊 主要是在前端做處理,需添加引用System.Windows.Input 和 System.Windows.Interactivity.dll,如果用第三方MVVM框架就不需要這個dll)
1)TextBox輸入框的鍵盤回車Enter事件
<TextBox Text="">
<!--回車事件--> <TextBox.InputBindings> <KeyBinding Key="Enter" Command="{Binding BtnB}" /> </TextBox.InputBindings>
<!--文本內容發生更改的事件--> <i:Interaction.Triggers> <i:EventTrigger EventName="TextChanged"> <i:InvokeCommandAction Command="{Binding BtnA}" /> </i:EventTrigger> </i:Interaction.Triggers>
<!--鼠標雙擊事件--> <i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding DoubleClickTxT}"
CommandParameter="{Binding Freight}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>CommandParameter是事件觸發時的參數,可不寫;
可以用前面RelayCommand的方法寫事件,也可以直接用ICommand寫事件且事件的方法可以寫在get里面,如:/// 雙擊輸入框事件 [復制當前文字到粘貼板] public ICommand DoubleClickTxT { get { return new DelegateCommand<object>((selectItem) => { CopyMsg = Convert.ToString(selectItem); if (string.IsNullOrWhiteSpace(CopyMsg)) return; Clipboard.SetDataObject(selectItem); }); } }Clipboard是粘貼板;
string.IsNullOrWhiteSpace判斷字符串是否為NULL值或者空值;
2)DataGrid的鼠標雙擊事件
<DataGrid x:Name="DgvReceiveOrder" ItemsSource="{Binding LstReceiveOrder}"> <DataGrid.InputBindings> <MouseBinding Gesture="LeftDoubleClick"
Command="{Binding DgvDoubleClick}"
CommandParameter="{Binding ElementName=DgvReceiveOrder,Path=SelectedItem}"/> </DataGrid.InputBindings> </DataGrid>單擊事件可以直接寫在SelectedItem里面;
3.輸入框的限制
背景:遇到只需要用戶輸入數字,價格和重量等不需要特殊字符和字母;
解決方式:
1)用上面講述的TextChanged作控制,但是不確定是否我調用不當,導致有些bug異常不清楚如何調整;
這里我就不貼代碼了,由於有bug,已經把代碼刪掉,用下面兩種方法實現效果吧;
2)由於這個字符限制不涉及業務邏輯代碼,可以不用MVVM的模式,用windowsFrom的后台代碼模式進行限制;
2.1)前端xaml代碼
<TextBox TextChanged="TxtWeightTextChanged" />2.2)前端xaml的后台cs代碼
public void TxtWeightTextChanged(object sender, TextChangedEventArgs e) { TextValueChanged(e, TxtWeight); } public static void TextValueChanged(
TextChangedEventArgs e, TextBox txtInput, string txType = "double") { var change = new TextChange[e.Changes.Count]; e.Changes.CopyTo(change, 0); var offset = change[0].Offset; if (change[0].AddedLength <= 0) return; if (txType == "int") { int num; if(string.IsNullOrWhiteSpace(txtInput.Text))return; if (int.TryParse(txtInput.Text, out num)) return; txtInput.Text = txtInput.Text.Remove(
offset, change[0].AddedLength); txtInput.Select(offset, 0); } else if (txType == "double") { double num; if(string.IsNullOrWhiteSpace(txtInput.Text))return; if (double.TryParse(txtInput.Text, out num)) return; txtInput.Text = txtInput.Text.Remove(
offset, change[0].AddedLength); txtInput.Select(offset, 0); } }這里利用了 Int 和 Double 的 tryParse 處理;能轉換則轉換,轉換不了 則移除異常的字符;
3)string類型字符作正則表達式限制處理;
3.1)前端xaml代碼
<TextBox input:InputMethod.IsInputMethodEnabled="False"
Text="{Binding RowNumber}" />3.2)后台ViewModel代碼
public double InputWeight { get => _inputWeight; set { _inputWeight = value; RowNumber = $"{_inputWeight}"; RaisePropertyChanged("Height"); } } private double _inputWeight; //[RowNumber]與[InputWeight]相互結合使用,前端綁定RowNumber,后端處理數據用InputWeight; //前端要用上雙向綁定,且綁定更新:Mode=TwoWay,UpdateSourceTrigger=PropertyChanged; //建議開啟 input:InputMethod.IsInputMethodEnabled="False" 限制不能切換中文輸入法; //input是xmlns:input="clr-namespace:System.Windows.Input;assembly=PresentationCore" public string RowNumber { get => _rowNumber; set { _rowNumber = ExtractDouble(value); //int類型,就把double置換成int var isdouble = double.TryParse(_rowNumber, out var raiseNumber); if (isdouble && $"{InputWeight}" != $"{raiseNumber}") InputWeight = raiseNumber; //這里賦值真正需要處理的 RaisePropertyChanged("RowNumber"); } } private string _rowNumber; /// 判斷字符串非doule字符,則提取數字 private string ExtractDouble(string str) { if (string.IsNullOrWhiteSpace(str)) return str; var isdouble = double.TryParse(str, out var _); if (isdouble) return str; if (!Regex.IsMatch(str, @"^\d+(\.\d+)?$")) str = Regex.Replace(str, @"[^\d.\d]", ""); if (str.Split('.').Length > 2) str = $"{str.Split('.')[0]}.{str.Split('.')[1]}"; return str; }后台處理業務邏輯代碼的時候用InputWeight;
畢竟前端的輸入是允許空和數字+小數點,但在后台程序處理的時候,double是不接受空值和有小數點但后面沒數字這個異常字符的;
4.異步的處理
背景:訪問站點接口和操作數據庫,或者處理一些耗時的業務場景時,會造成前端頁面的假死和卡頓,這時候需要運用異步線程處理,業務邏輯代碼后台自動跑,前端提供用戶繼續操作其他事項或者看到進度條有進度變化;
解決方式:
異步代碼用Task
Task.Factory.StartNew(() => TaskSubmit(param)).ContinueWith(task => ContinueSubmit(task.Result));[ TaskSubmit ] 方法是對站點接口或者對數據庫的交互處理;
[ ContinueSubmit ] 方法是在”TaskSubmit”方法執行完畢后再執行的后續操作;
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => { //TODO 使用該語句切換回主UI線程操作界面控件數據; }));備注:建議在異步的時候加上 try{ }catch{ } 語句和寫日志的語句防止出現異常時 便於查閱情況;
這里不提供詳細代碼,如需了解則關注后續關於數據庫操作的項目代碼!
5.隱藏和可用狀態
背景:遇到業務場景需要,在觸發事件的業務邏輯處理完畢后需要隱藏或者設置某些控件不可用,就需要View和ViewModel之間的屬性關聯;
解決方式:
貼圖,懶得敲代碼(大同小異),知道思路就OK;
6.自定義屬性 (略)
因為使用了自定義屬性導致在開發狀態中的設計視圖不可視,造成開發困惱且暫時未找到解決方案,就不喜歡用自定義屬性,也因此該代碼暫略,后期有時間再補充;
7.ComboBox的輸入中文拼音和簡寫的模糊查詢;
1) 前端xaml代碼
//頭部需含引用: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" System.Windows.Interactivity.dll<ComboBox MinWidth="80" Height="40" Tag="選擇國家..."
DisplayMemberPath="Value" Text="{Binding CountryName}" Style="{StaticResource ComboBoxStyle}"
IsDropDownOpen="{Binding CountryOpen}" ItemsSource="{Binding CountryData}"
SelectedItem="{Binding CountrySelect}"> <i:Interaction.Triggers> <i:EventTrigger EventName="KeyUp"> <i:InvokeCommandAction Command="{Binding CountryKeyUp}"
CommandParameter="{Binding CountryName}"/> </i:EventTrigger> </i:Interaction.Triggers> </ComboBox>2)ViewModel后台代碼
/// 基礎數據視圖模型: public class BasicDataViewModel : BindableBase { public BasicDataViewModel() {
//獲取緩存中的國家信息數據 var countryData = new Service.BasicDataCacheManager().AllCountrys .OrderBy(t => t.FsName).ToList();
//初始化一個下拉列表模板 var countrys = new List<TemplateViewModel>();
//遍歷數據重新篩選需要的value和key值 foreach (var tmp in countryData) {
//調用轉換成拼音的方法把中文翻譯成拼音 var pinYinRet = Common.PinYinHelper.ToPinYin(tmp.FsName);
//SerachCode字段是查詢字典 var model = new TemplateViewModel { Key = tmp.Id, Value = tmp.FsName, SearchCode = new List<string>{tmp.Id,tmp.FsName,tmp.EnName} };
//把拼音添加到字典,也可以把英文添加到字典,自由發揮 if (pinYinRet != null) { if(pinYinRet.FirstPingYin != null && pinYinRet.FirstPingYin.Any()) model.SearchCode.AddRange(pinYinRet.FirstPingYin); if(pinYinRet.FullPingYin != null && pinYinRet.FullPingYin.Any()) model.SearchCode.AddRange(pinYinRet.FullPingYin); } countrys.Add(model); } CountryData = countrys; _baseCountryData = countrys; CountrySelect = new TemplateViewModel(); } /// 基礎國家數據 【全部】 private readonly List<TemplateViewModel> _baseCountryData; /// 郵遞方式ID 對應key public string CountryId { get { if (string.IsNullOrWhiteSpace(_countryId)
&& !string.IsNullOrWhiteSpace(CountryName)) {
//如果key為空而輸入框的中文不為空,則匹配出第一條符合的數據 _countryId = _baseCountryData.FirstOrDefault(t =>
t.Value == CountryName)?.Key; } return _countryId; } set => _countryId = value; } private string _countryId; /// 國家中文名稱 對應value public string CountryName { get => _countryName; set { _countryName = value; if (string.IsNullOrWhiteSpace(_countryName)) { CountryId = string.Empty; CountrySelect = new TemplateViewModel(); } RaisePropertyChanged("CountryName"); } } private string _countryName; /// 國家 public List<TemplateViewModel> CountryData { get => _countryData; set { _countryData = value; RaisePropertyChanged("CountryData"); } } private List<TemplateViewModel> _countryData; /// 選中的國家 public TemplateViewModel CountrySelect { get => _countrySelect; set { _countrySelect = value; if (!string.IsNullOrWhiteSpace(_countrySelect.Key)) { CountryId = _countrySelect.Key; CountryName = _countrySelect.Value; } RaisePropertyChanged("CountrySelect"); } } private TemplateViewModel _countrySelect; /// 國家下拉內容的顯示 public bool CountryOpen { get => _countryOpen; set { _countryOpen = value; RaisePropertyChanged("CountryOpen"); } } private bool _countryOpen; /// 國家輸入框鍵盤鍵入事件 public ICommand CountryKeyUp { get { return new DelegateCommand<string>((str) => { var matchItem = string.IsNullOrWhiteSpace(str) ? _baseCountryData : (from item in _baseCountryData where !string.IsNullOrEmpty(item.Key) where item.SearchCode.Any(code =>
code.ToLower().Contains(str.ToLower())) select new TemplateViewModel { Key = item.Key, Value = item.Value, SearchCode = item.SearchCode }).ToList(); CountryData = matchItem; CountryOpen = true; }); } } }使用了多個事件,自行慢慢體會吧,估計以上代碼還有優化的余地;
拼音的方法類 和 中英翻譯的方法類就不提供了,
從緩存中獲取數據的代碼也不提供,畢竟涉及到數據庫操作;后期段章中再作描述;
3)執行效果
8.注:該文章代碼全在上面,如需詳細代碼和視頻再私信作者!
9.下篇預告
分頁控件的制作,郵件發送,日志代碼,excel導入導出等代碼的實現過程;






