WPF项目学习.二

时间:2022-02-15 22:16:50

WPF用MVVM的解决记录

版权声明:本文为博主初学经验,未经博主允许不得转载。

一、前言

  记录在学习与制作WPF过程中遇到的解决方案。

   焦点的控制,键盘事件触发,输入框的数字限制,异步处理,隐藏状态可用状态,自定义属性等等...

二、配置

系统环境:win10

开发工具:Visual Studio 2017

开发语言:C#.WPF (MVVM框架)

三、自问自答

  1.焦点的控制;

  背景:

  焦点的使用一般用于输入框,切换业务功能时,需要焦点定位在指定输入框位置,便于用户操作;使用MVVM框架开发后,对于前端控件的焦点控制不便调用,控件绑定的都是偏向于文本内容和事件,不像windowsFrom那般直接调用控件的焦点属性;

  解决方式:

1)自定义属性;

  下面第6点的说明;

  

2)前端布局所需焦点,后端遍历;

  在Grid的Style样式里面设置好需要布置焦点的触发器;

  后期每次更改焦点前,都把所有焦点触发器设置为false,然后在更改指定的焦点为true;

2.1) 前端xaml代码

WPF项目学习.二

<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="">
<StackPanel Orientation="Horizontal">
<TextBox x:Name="TxtA" Text="" Width="" Height=""
          Tag="输入框A..." Style="{StaticResource TxbTrigger}"/>
<TextBox x:Name="TxtB" Text="" Width="" Height="" Margin=""
          Tag="输入框B..." Style="{StaticResource TxbTrigger}"/>
<TextBox x:Name="TxtC" Text="" Width="" Height=""
          Tag="输入框C..." Style="{StaticResource TxbTrigger}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Content="焦点A" Width="" Height="" Foreground="White"
        Command="{Binding BtnA}" Template="{StaticResource DefaultButton}"/>
<Button Content="焦点B" Width="" Height="" Foreground="White" Margin="20"
        Command="{Binding BtnB}" Template="{StaticResource DefaultButton}"/>
<Button Content="焦点C" Width="" Height="" 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) 执行效果

WPF项目学习.二

  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, );
var offset = change[].Offset;
if (change[].AddedLength <= ) 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[].AddedLength);
txtInput.Select(offset, );
}
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[].AddedLength);
txtInput.Select(offset, );
}
}

这里利用了 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 > )
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之间的属性关联;

    解决方式:

WPF项目学习.二

WPF项目学习.二

WPF项目学习.二

贴图,懒得敲代码(大同小异),知道思路就OK;

  6.自定义属性 (略)

    因为使用了自定义属性导致在开发状态中的设计视图不可视,造成开发困恼且暂时未找到解决方案,就不喜欢用自定义属性,也因此该代码暂略,后期有时间再补充;

  7.ComboBox的输入中文拼音和简写的模糊查询;

1) 前端xaml代码

//头部需含引用:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
System.Windows.Interactivity.dll
<ComboBox MinWidth="" Height="" 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)执行效果

WPF项目学习.二

  8.注:该文章代码全在上面,如需详细代码和视频再私信作者!

  9.下篇预告

分页控件的制作,邮件发送,日志代码,excel导入导出等代码的实现过程;