在WPF中减少逻辑与UI元素的耦合
在WPF中减少逻辑与UI元素的耦合
周银辉
1, 避免在逻辑中引用界面元素,别把后台数据强加给UI
一个糟糕的案例
比如说主界面上有一个显示当前任务状态的标签label_TaskState,我们会时常更新该标签以便及时地将任务状态通知用户。那么很糟糕的一种假设是我们的代码中会到处充斥着这样的语句段this.label_TaskState .Content = this.GetStateDescription(TaskStates.Busy);(GetStateDescription方法会返回一段比较友好的描述信息)
当用户点击“暂停”按钮后,我们可能要这样来这样更新标签:
void btn_Pause_Clicked(object sender, RoutedEventArgs e)
{
//do something to pause the task
//update our lab
this.label_TaskState .Content = this.GetStateDescription(TaskStates.Pause);
}
当由于某种原因我们的任务发生了错误时,我们可能会这样:
try
{
//do something dangerous
}
catch(MyException e)
{
this.label_TaskState .Content = this.GetStateDescription(TaskStates.Error);
}
finally
{
//…
}
这样一来,我们的逻辑代码无数地方将引用label_TaskState这个UI元素。
现在有一些变化来了:(1)我们觉得使用一段文本来描述任务状态还是不够直观,所以我们决定使用美工提供的一系列漂亮图标来显示当前状态(图标中也可能含有文字,不过我们不关心)。(2)另外一个面板上(myPanel2)也要放置一个显示任务当前任务状态的标签label_TaskState2,只不过其仅仅显示文字描述就可以了。
那么我们在这么糟糕的环境下是不是应该像这样来修改我们的代码呢?
首先找出所有引用了label_TaskState的地方(比如有20个)。
然后将Lable类型的label_TaskState控件修改为Image类型的image_TaskState控件。
然后重复地将this.label_TaskState .Content = this.GetStateDescription(TaskStates.Busy);语句替换为this.image_TaskState.Source = this.GetStateImage(TaskStates.Busy);
别忘了每次都要在该语句后追加一条:this.label_TaskState2.Content = this.GetStateDescription(TaskStates.Busy);因为我们增加了一个标签。
多么令人上火的编程工作啊。
原因是,我们频繁地引用不稳定的界面元素(label_TaskState),严重地将界面和逻辑耦合在了一起,我们采用赋值的方式将后台数据(当前状态信息)强加给了UI元素。
解决方案:使用Binding,然UI元素从后台“拿”数据
一个简单的描述是:后台逻辑对前台UI说“要如何展现由前台决定,数据就在这里,要用就自己来拿吧”
“数据就在这里”
我们的数据是当前任务的状态信息,为了提供给UI元素和后台逻辑使用,我们决定提供一个TheTaskState属性来跟踪当前状态:
public TaskStates TheTaskState
{
get
{
return (TaskStates)GetValue(TheTaskStateProperty);
}
set
{
SetValue(TheTaskStateProperty, value);
}
}
public static readonly DependencyProperty TheTaskStateProperty =
DependencyProperty.Register("TheTaskState", typeof(TaskStates),
typeof(Window1), new UIPropertyMetadata(TaskStates.Idle));
这样后台逻辑中要改变任务状态时只需要修改TheTaskState属性就可以了。
“要用就自己来拿吧”
当前台需要向用户展现该任务状态时只需要读取该属性,要实时跟踪就绑定吧。
<Label x:Name="label_TaskState"
Content="{Binding ElementName=windowMain,Path=TheTaskState}" />
“要如何展现由前台决定”
不对,我要展现给用户的可不是一些枚举值,而应该是图片或文本。
的确如此,所以我们要在绑定中加入转换器(或者数据模板,这里我们使用转换器):
public class TaskStatesImageConverter:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
TaskStates state = (TaskStates)value;
return GetImageFromTaskState(state);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
private Image GetImageFromTaskState(TaskStates state)
{
Image image = new Image();
image.Source = new BitmapImage(new Uri((int)state+".png", UriKind.Relative));
return image;
}
}
<Label x:Name="label_TaskState"
Content="{Binding ElementName=windowMain,
Path=TheTaskState,
Converter={StaticResource myTaskStatesImageConverter}}" />
这样一来,我们的后台逻辑没有去引用UI元素并把数据强加给它,后台关注于如何任务状态及其更新,前台专注于如何向用户展现这些信息。当我们要更换其他展示方式时,只需更换一下转换器就可以了。
2,避免逻辑代码依赖Template中的元素
Template的目的是可更换,如果和逻辑耦合在一起就很有可能在更换Template的时候出现异常,也就是不可更换,模板就失去了意义。
糟糕的案例1
比如让我们来打造ScrollBar这样的控件,如果按照如下的方式来处理用户点击上ScrollBar两端的箭头就会出现问题:在ScrollBar的ControlTemplate的视觉树的两端分别是放置一个ToggleButton,以便用户点击这两个按钮可以上下(或左右)翻页,如何处理用户的点击事件呢?错误的方式是注册按钮的点击事件:
private void RepeatButton1_Click(object sender, RoutedEventArgs e)
{
RepeatButton rb = (RepeatButton)sender;
// left(or up) scrolling
}
private void RepeatButton2_Click(object sender, RoutedEventArgs e)
{
RepeatButton rb = (RepeatButton)sender;
// right(or down) scrolling
}
糟糕的案例2
有时会犯这样的错误:本来我们遵守着很多规范地使用ControlTemplate(DataTemplate是一样的道理)来将逻辑和UI很好的分开了(比如我们打造了一个不错的CustromControl),但突然发现似乎要在逻辑代码中引用ControlTemplate视觉树中的某个元素,然后发现FrameworkTemplate.FindName()可以完成这项工作,便出现了下面这样的代码:
<Style TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid Margin="5" Name="grid">
<!--someting else-->
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Grid gridInTemplate = (Grid)myButton1.Template.FindName("grid", myButton1);
//do something about the grid
这些都可以完成工作,但我们知道在WPF中,用户(你的控件用户,也可能是你自己)是可以定制ControlTemplate的,那么用户完全可以将逻辑引用到的这两个RepeatButton删掉或更换成其它元素,那么控件必定残缺甚至异常。如果不允许用户更改则失去了Template的意义。
解决方案:
经验是,当你觉得必须对视觉树中的元素进行事件注册以便挂接到某个事件处理方法上时,你可以想办法将方法所实现的功能包装成Command。比如案例1中,可以将滚动条的上下(或左右)翻页包装成形如ScrollBar.LineUpCommand、ScrollBar.LineDownCommand的形式,然后只需将视觉树中的表示上下翻页的元素的Command属性指定成他们就可以了。若仅仅是元素的某个事件将改变某些元素(或自己)的状态时,你可以使用Trigger来达到这一目的(也许你需要增加一些Dependency Property来充当Trigger的条件),比如:
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="Bg" Value="Red "/>
</Trigger>
</ControlTemplate.Triggers>
绝对没有借口让逻辑部分引用DataTemplate的元素,可能会有逻辑引用ContorlTemplate中的元素的情况(打造某些CustomControl时),这时你可以使用TemplatePartAttribute来进行标识。
打造CustomControl时遇到的耦合问题,可以参考这篇文章:在WPF中自定义控件(3) CustomControl (下)
3,总结
总的说来,我们应该为逻辑和UI的解耦而努力,WPF也为我们提供了这样的机制。上面的例子仅仅是说明了常见的几种情况,而提供的解决方案也仅供参考,没有放之四海而皆准的方案,因为这其中涉及到了太多适应于不同情况下的小技巧。但总体而言:数据绑定、Style,Template,Command,Resource等为逻辑和UI的解耦提供了几条途径,如果你发现你的逻辑代码和UI元素严重地耦合在了一起而带来了不少麻烦,那么可以从上面的几条途径入手。另外,写这篇文字的最主要目的还是引起大家在实际编码过程中对逻辑和UI的解耦的重视。
在WPF中减少逻辑与UI元素的耦合的更多相关文章
- [WPF自定义控件]?Window(窗体)的UI元素及行为
原文:[WPF自定义控件]?Window(窗体)的UI元素及行为 1. 前言 本来打算写一篇<自定义Window>的文章,但写着写着发觉内容太多,所以还是把使用WindowChrome自定 ...
- 拒绝卡顿——在WPF中使用多线程更新UI
原文:拒绝卡顿--在WPF中使用多线程更新UI 有经验的程序员们都知道:不能在UI线程上进行耗时操作,那样会造成界面卡顿,如下就是一个简单的示例: public partial class MainW ...
- WPF中的逻辑树和可视化树
WPF中的逻辑树是指XAML元素级别的嵌套关系,逻辑树中的节点对应着XAML中的元素. 为了方便地自定义控件模板,WPF在逻辑树的基础上进一步细化,形成了一个“可视化树(Visual Tree)”,树 ...
- WPF 中的逻辑树(Logical Tree)与可视化元素树(Visual Tree)
一.前言 WPF 中有两种"树":逻辑树(Logical Tree)和可视化元素树(Visual Tree). Logical Tree 最显著的特点就是它完全由布局组件和控件 ...
- [WPF自定义控件]Window(窗体)的UI元素及行为
1. 前言 本来打算写一篇<自定义Window>的文章,但写着写着发觉内容太多,所以还是把使用WindowChrome自定义Window需要用到的部分基础知识独立出来,于是就形成了这篇文章 ...
- CSharpGL(6)在OpenGL中绘制UI元素
CSharpGL(6)在OpenGL中绘制UI元素 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo,更适合入 ...
- 在WPF中自定义控件
一, 不一定需要自定义控件在使用WPF以前,动辄使用自定义控件几乎成了惯性思维,比如需要一个带图片的按钮,但在WPF中此类任务却不需要如此大费周章,因为控件可以嵌套使用以及可以为控件外观打造一套新的样 ...
- WPF中的命令与命令绑定(二)
原文:WPF中的命令与命令绑定(二) WPF中的命令与命令绑定(二) 周银辉在WPF中,命令(Commandi ...
- Silverlight及WPF中实现自定义BusyIndicator
在开发Silverlight或者WPF项目时,当我们调用Web服务来加载一些数据时,由于数据量比较大需要较长的时间,需要用户等待,为了给用户友好的提示和避免用户在加载数据过程中进行重复操作,我们通常使 ...
随机推荐
- html5 Canvas和SVG的区别是什么(总结)
html5 Canvas和SVG的区别是什么(总结) 一.总结 一句话总结:都是2D做图,svg是矢量图,canvas是位图.Canvas 是逐像素进行渲染的,适合游戏. 1.svg的全称是什么? S ...
- Ajax的get、post和ajax提交
JQuery.get(url,[data],[callback],[type]) []里面的参数是可选的,不是必填的. [data]:带发送的key/value数据. [callback]:载入成功时 ...
- sql for xml 还有一种写法(採用 tag 与 union all,简洁易懂)
sql for xml 还有一种写法(採用 tag 与 union all,简洁易懂) 測试环境:sql 08, 08 R2, 2010, 2012, 2014 等 declare @agent t ...
- STL map 按key值和按value值排序
map是用来存放<key, value>键值对的数据结构,能够非常方便高速的依据key查到对应的value. 假如存储水果和其单位价格.我们用map来进行存储就是个不错的选择. 我们这样定 ...
- 切换-5.7-传统复制切换成GTID复制
1.基本环境: Master Slave MySQL版本 MySQL-5.7.16-X86_64 MySQL-5.7.16-X86_64 IP 192.168.56.156 192.168.5 ...
- Java中sleep()与wait()区别
学习时正好碰到这两个方法,就查阅相关资料,并通过程序实现,进行区别一下: 1.每个对象都有一个锁来控制同步访问,Synchronized关键字可以和对象的锁交互,来实现同步方法或同步块.sleep() ...
- Android系统编译环境初始化时Product产品的import-nodes过程
从运行make -f config,mk文件開始,config,mk作为当前的makefile文件.将会被make解析,一般make解析Makefile文件流程首先是载入当中include的各种其它m ...
- [Ramda] Refactor to a Point Free Function with Ramda's useWith Function
Naming things is hard and arguments in generic utility functions are no exception. Making functions ...
- amazeui中css组件、js组件、web组件的区别
amazeui中css组件.js组件.web组件的区别 一.总结 一句话总结: 1.可直接像调用js插件那样调用:在AmazeUI(妹子UI)中,Web组件可以不编写模板而直接使用,若如此,则与JS插 ...
- Redis管理及监控工具
Redis管理及监控工具 Redis做为现在web应用开发的黄金搭担组合,大量的被应用,广泛用于存储session信息,权限信息,交易作业等热数据.做为一名有10年以上JAVA开发经验的程序员,工作中 ...