原文:在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元素的耦合的更多相关文章

  1. [WPF自定义控件]?Window(窗体)的UI元素及行为

    原文:[WPF自定义控件]?Window(窗体)的UI元素及行为 1. 前言 本来打算写一篇<自定义Window>的文章,但写着写着发觉内容太多,所以还是把使用WindowChrome自定 ...

  2. 拒绝卡顿——在WPF中使用多线程更新UI

    原文:拒绝卡顿--在WPF中使用多线程更新UI 有经验的程序员们都知道:不能在UI线程上进行耗时操作,那样会造成界面卡顿,如下就是一个简单的示例: public partial class MainW ...

  3. WPF中的逻辑树和可视化树

    WPF中的逻辑树是指XAML元素级别的嵌套关系,逻辑树中的节点对应着XAML中的元素. 为了方便地自定义控件模板,WPF在逻辑树的基础上进一步细化,形成了一个“可视化树(Visual Tree)”,树 ...

  4. WPF 中的逻辑树(Logical Tree)与可视化元素树(Visual Tree)

    一.前言 ​ WPF 中有两种"树":逻辑树(Logical Tree)和可视化元素树(Visual Tree). Logical Tree 最显著的特点就是它完全由布局组件和控件 ...

  5. [WPF自定义控件]Window(窗体)的UI元素及行为

    1. 前言 本来打算写一篇<自定义Window>的文章,但写着写着发觉内容太多,所以还是把使用WindowChrome自定义Window需要用到的部分基础知识独立出来,于是就形成了这篇文章 ...

  6. CSharpGL(6)在OpenGL中绘制UI元素

    CSharpGL(6)在OpenGL中绘制UI元素 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo,更适合入 ...

  7. 在WPF中自定义控件

    一, 不一定需要自定义控件在使用WPF以前,动辄使用自定义控件几乎成了惯性思维,比如需要一个带图片的按钮,但在WPF中此类任务却不需要如此大费周章,因为控件可以嵌套使用以及可以为控件外观打造一套新的样 ...

  8. WPF中的命令与命令绑定(二)

    原文:WPF中的命令与命令绑定(二) WPF中的命令与命令绑定(二)                                              周银辉在WPF中,命令(Commandi ...

  9. Silverlight及WPF中实现自定义BusyIndicator

    在开发Silverlight或者WPF项目时,当我们调用Web服务来加载一些数据时,由于数据量比较大需要较长的时间,需要用户等待,为了给用户友好的提示和避免用户在加载数据过程中进行重复操作,我们通常使 ...

随机推荐

  1. Linux快捷键、文件管理和查询

    Linux学习(1)-常用快捷键.文件管理和查询   阅读目录   有话要说 Shell应用技巧,常用快捷键 帮助命令 man (帮助命令) info (和man差不多) whatis (查询基本信息 ...

  2. RSA算法原理(转)

    如果你问我,哪一种算法最重要?我可能会回答“公钥加密算法”.因为它是计算机通信安全的基石,保证了加密数据不会被破解.你可以想象一下,信用卡交易被破解的后果. 进入正题之前,我先简单介绍一下,什么是”公 ...

  3. The DOT Language

    CSDN新首页上线啦,邀请你来立即体验! 立即体验 博客 学院 下载 更多 登录注册 The DOT Language 翻译 2014年04月15日 11:27:07 标签: EBNF / 语言 / ...

  4. [Javascript] Format console.log with CSS and String Template Tags

    The Chrome console allows you to format messages using CSS properties. This lesson walks you through ...

  5. 【codeforces 755D】PolandBall and Polygon

    time limit per test4 seconds memory limit per test256 megabytes inputstandard input outputstandard o ...

  6. [NPM] Run npm scripts when files change with onchange

    In this lesson we will look at how we can setup our npm scripts to execute when the file system has ...

  7. Android 离线语音用法(讯飞语音)

    这次给大家带来的是项目的离线语音功能. 讯飞开放平台中的离线语音 首先创建开放平台的账号.这个不必多说 然后创建新应用 选择我的应用,例如以下图,注意下我打马赛克的地方,这个appId非常重要 点击进 ...

  8. HDU 5317 RGCDQ(素数个数 多校2015啊)

    题目链接:pid=5317" target="_blank">http://acm.hdu.edu.cn/showproblem.php?pid=5317 Prob ...

  9. 【37.50%】【codeforces 732D】Exams

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  10. PCI GXL学习之安装篇

    作者:朱金灿 来源:http://blog.csdn.net/clever101 上周简单学习了PCI gxl的使用和二次开发.说实话gxl没有我想象中那么自动化,我原以为把一个数据处理作业扔给gxl ...