inotifyxqq登录验证过程程中提示被某个应用阻拦导致无法通过怎么办,怎么可以知道是什么应用?(小米8)

  两个月前发了两篇有关监听自己是否被卸载和卸载反馈功能实现的博客,第二版的地址如下:,感谢@whiletrue_童鞋发现了我的代码在4.2.2系统上无法实现卸载反馈,经过调试,有了问题的解决方案,但是由于发完博客后即处于闭关开发阶段,没时间打理博客,所以解决方案迟迟没有与大家见面,最近空闲下来,将解决思路及方案发出来给大家看看还有没有问题。
  调试发现,监听依然没有问题,毕竟是Linux Kernel中的接口,Framework层再怎么改也改不到那儿去,那么问题出在哪呢?阻塞结束后,通过调用exec函数发出am命令调起浏览器访问网页,在API16(Android 4.1.x)的设备上尚可正常访问网页,而API17(Android 4.2.x)的设备上连浏览器也不能调起。
  通过分析log,发现了一条线索,如下面的log的所示:
W/ActivityManager( 387): Permission Denial: startActivity asks to run as user -2 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL
  log中直接给出提示,需要加一个权限INTERACT_ACROSS_USERS_FULL,这个权限时API17新引入的,目的在于允许不同用户的应用之间可以产生交互。可是加上去之后发现,还不是无法调起浏览器,而且log依然提示需要权限INTERACT_ACROSS_USERS_FULL,很是奇怪,于是继续分析。
  首先说明一下Linux中的pid和uid,以及android扩展的userSerialNumber。pid是Process的标识,用于系统对进程的控制,从API层面看就是用于Process.killProcess()和Process.sendSignal();uid在Linux系统中是用来标识用户的,而在android将uid视为app的标识id,用于"sandbox"安全模型,即用于app权限控制;而对于API17引入的多用户支持(目前只支持平板),uid已经被占用,只好新引入userSerialNumber来标识用户。
  回到刚才的问题,log中告知startActivity时运行用户标识为-2,而调用却是由用户标识0发起,导致拒绝执行。用这句话搜索,发现在Google开发者网站中有相关的issue,链接如下:(打不开可以把https改为http)。结合官方的回答,问题原因如下:由于被卸载,C端进程监听到目录被删除,立即执行am命令,此时将会默认以USER_CURRENT的身份执行,由于API17中ActivityManagerService.handleIncomingUser()会校验userSerialNumber,发现用户标识不匹配,导致权限校验失败&&这也说明了权限的影响范围仅限于Java端的进程,对于fork()出来的C端进程来说,并不继承父进程在Android中声明的权限。
  解决方案:增加处理分支,若API&=17,将userSerialNumber传递给C端进程,然后在am命令中带上参数--user userSerialNumber即可。
Java端代码如下:
1 package main.
3 import java.lang.reflect.InvocationTargetE
4 import java.lang.reflect.M
6 import pym.test.uninstalledobserver.R;
7 import android.app.A
8 import android.os.B
9 import android.os.B
10 import android.util.L
* @author pengyiming
* @note 监听此应用是否被卸载,若被卸载则弹出卸载反馈
* @note 由于API17加入多用户支持,原有命令在4.2及更高版本上执行时缺少userSerial参数,特此修改
19 public class UninstalledObserverActivity extends Activity
/* 数据段begin */
private static final String TAG = "UninstalledObserverActivity";
// 监听进程pid
private int mObserverProcessPid = -1;
/* 数据段end */
/* static */
// 初始化监听进程
private native int init(String userSerial);
Log.d(TAG, "load lib --& uninstalled_observer");
System.loadLibrary("uninstalled_observer");
/* static */
/* 函数段begin */
public void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.uninstalled_observer_layout);
// API level小于17,不需要获取userSerialNumber
if (Build.VERSION.SDK_INT & 17)
mObserverProcessPid = init(null);
// 否则,需要获取userSerialNumber
mObserverProcessPid = init(getUserSerial());
protected void onDestroy()
super.onDestroy();
// 示例代码,用于结束监听进程
if (mObserverProcessPid & 0)
android.os.Process.killProcess(mObserverProcessPid);
// 由于targetSdkVersion低于17,只能通过反射获取
private String getUserSerial()
Object userManager = getSystemService("user");
if (userManager == null)
Log.e(TAG, "userManager not exsit !!!");
return null;
Method myUserHandleMethod = android.os.Process.class.getMethod("myUserHandle", (Class&?&[]) null);
Object myUserHandle = myUserHandleMethod.invoke(android.os.Process.class, (Object[]) null);
Method getSerialNumberForUser = userManager.getClass().getMethod("getSerialNumberForUser", myUserHandle.getClass());
long userSerial = (Long) getSerialNumberForUser.invoke(userManager, myUserHandle);
return String.valueOf(userSerial);
catch (NoSuchMethodException e)
Log.e(TAG, "", e);
catch (IllegalArgumentException e)
Log.e(TAG, "", e);
catch (IllegalAccessException e)
Log.e(TAG, "", e);
catch (InvocationTargetException e)
Log.e(TAG, "", e);
return null;
/* 函数段end */
核心&&native方法头文件:
1 /* 头文件begin */
2 #include &jni.h&
3 #include &stdlib.h&
4 #include &stdio.h&
5 #include &string.h&
6 #include &unistd.h&
7 #include &fcntl.h&
8 #include &sys/inotify.h&
9 #include &sys/stat.h&
11 #include &android/log.h&
12 /* 头文件end */
14 /* 宏定义begin */
15 //清0宏
16 #define MEM_ZERO(pDest, destSize) memset(pDest, 0, destSize)
18 //LOG宏定义
19 #define LOG_INFO(tag, msg) __android_log_write(ANDROID_LOG_INFO, tag, msg)
20 #define LOG_DEBUG(tag, msg) __android_log_write(ANDROID_LOG_DEBUG, tag, msg)
21 #define LOG_WARN(tag, msg) __android_log_write(ANDROID_LOG_WARN, tag, msg)
22 #define LOG_ERROR(tag, msg) __android_log_write(ANDROID_LOG_ERROR, tag, msg)
23 /* 宏定义end */
25 #ifndef _Included_main_activity_UninstalledObserverActivity
26 #define _Included_main_activity_UninstalledObserverActivity
27 #ifdef __cplusplus
28 extern "C" {
31 #undef main_activity_UninstalledObserverActivity_MODE_PRIVATE
32 #define main_activity_UninstalledObserverActivity_MODE_PRIVATE 0L
33 #undef main_activity_UninstalledObserverActivity_MODE_WORLD_READABLE
34 #define main_activity_UninstalledObserverActivity_MODE_WORLD_READABLE 1L
35 #undef main_activity_UninstalledObserverActivity_MODE_WORLD_WRITEABLE
36 #define main_activity_UninstalledObserverActivity_MODE_WORLD_WRITEABLE 2L
37 #undef main_activity_UninstalledObserverActivity_MODE_APPEND
38 #define main_activity_UninstalledObserverActivity_MODE_APPEND 32768L
39 #undef main_activity_UninstalledObserverActivity_MODE_MULTI_PROCESS
40 #define main_activity_UninstalledObserverActivity_MODE_MULTI_PROCESS 4L
41 #undef main_activity_UninstalledObserverActivity_BIND_AUTO_CREATE
42 #define main_activity_UninstalledObserverActivity_BIND_AUTO_CREATE 1L
43 #undef main_activity_UninstalledObserverActivity_BIND_DEBUG_UNBIND
44 #define main_activity_UninstalledObserverActivity_BIND_DEBUG_UNBIND 2L
45 #undef main_activity_UninstalledObserverActivity_BIND_NOT_FOREGROUND
46 #define main_activity_UninstalledObserverActivity_BIND_NOT_FOREGROUND 4L
47 #undef main_activity_UninstalledObserverActivity_BIND_ABOVE_CLIENT
48 #define main_activity_UninstalledObserverActivity_BIND_ABOVE_CLIENT 8L
49 #undef main_activity_UninstalledObserverActivity_BIND_ALLOW_OOM_MANAGEMENT
50 #define main_activity_UninstalledObserverActivity_BIND_ALLOW_OOM_MANAGEMENT 16L
51 #undef main_activity_UninstalledObserverActivity_BIND_WAIVE_PRIORITY
52 #define main_activity_UninstalledObserverActivity_BIND_WAIVE_PRIORITY 32L
53 #undef main_activity_UninstalledObserverActivity_BIND_IMPORTANT
54 #define main_activity_UninstalledObserverActivity_BIND_IMPORTANT 64L
55 #undef main_activity_UninstalledObserverActivity_BIND_ADJUST_WITH_ACTIVITY
56 #define main_activity_UninstalledObserverActivity_BIND_ADJUST_WITH_ACTIVITY 128L
57 #undef main_activity_UninstalledObserverActivity_CONTEXT_INCLUDE_CODE
58 #define main_activity_UninstalledObserverActivity_CONTEXT_INCLUDE_CODE 1L
59 #undef main_activity_UninstalledObserverActivity_CONTEXT_IGNORE_SECURITY
60 #define main_activity_UninstalledObserverActivity_CONTEXT_IGNORE_SECURITY 2L
61 #undef main_activity_UninstalledObserverActivity_CONTEXT_RESTRICTED
62 #define main_activity_UninstalledObserverActivity_CONTEXT_RESTRICTED 4L
63 #undef main_activity_UninstalledObserverActivity_RESULT_CANCELED
64 #define main_activity_UninstalledObserverActivity_RESULT_CANCELED 0L
65 #undef main_activity_UninstalledObserverActivity_RESULT_OK
66 #define main_activity_UninstalledObserverActivity_RESULT_OK -1L
67 #undef main_activity_UninstalledObserverActivity_RESULT_FIRST_USER
68 #define main_activity_UninstalledObserverActivity_RESULT_FIRST_USER 1L
69 #undef main_activity_UninstalledObserverActivity_DEFAULT_KEYS_DISABLE
70 #define main_activity_UninstalledObserverActivity_DEFAULT_KEYS_DISABLE 0L
71 #undef main_activity_UninstalledObserverActivity_DEFAULT_KEYS_DIALER
72 #define main_activity_UninstalledObserverActivity_DEFAULT_KEYS_DIALER 1L
73 #undef main_activity_UninstalledObserverActivity_DEFAULT_KEYS_SHORTCUT
74 #define main_activity_UninstalledObserverActivity_DEFAULT_KEYS_SHORTCUT 2L
75 #undef main_activity_UninstalledObserverActivity_DEFAULT_KEYS_SEARCH_LOCAL
76 #define main_activity_UninstalledObserverActivity_DEFAULT_KEYS_SEARCH_LOCAL 3L
77 #undef main_activity_UninstalledObserverActivity_DEFAULT_KEYS_SEARCH_GLOBAL
78 #define main_activity_UninstalledObserverActivity_DEFAULT_KEYS_SEARCH_GLOBAL 4L
main_activity_UninstalledObserverActivity
* Signature: (Ljava/lang/S)V
85 JNIEXPORT int JNICALL Java_main_activity_UninstalledObserverActivity_init(JNIEnv *, jobject, jstring);
87 #ifdef __cplusplus
核心&&native方法实现:
1 /* 头文件begin */
2 #include "main_activity_UninstalledObserverActivity.h"
3 /* 头文件end */
5 #ifdef __cplusplus
6 extern "C"
10 /* 内全局变量begin */
11 static char TAG[] = "UninstalledObserverActivity.init";
12 static jboolean isCopy = JNI_TRUE;
14 static const char APP_DIR[] = "/data/data/pym.test.uninstalledobserver";
15 static const char APP_FILES_DIR[] = "/data/data/pym.test.uninstalledobserver/files";
16 static const char APP_OBSERVED_FILE[] = "/data/data/pym.test.uninstalledobserver/files/observedFile";
17 static const char APP_LOCK_FILE[] = "/data/data/pym.test.uninstalledobserver/files/lockFile";
18 /* 内全局变量 */
main_activity_UninstalledObserverActivity
* Signature: ()V
* return: 子进程pid
26 JNIEXPORT int JNICALL Java_main_activity_UninstalledObserverActivity_init(JNIEnv *env, jobject obj, jstring userSerial)
jstring tag = (*env)-&NewStringUTF(env, TAG);
LOG_DEBUG((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "init observer"), &isCopy));
// fork子进程,以执行轮询任务
pid_t pid = fork();
if (pid & 0)
LOG_ERROR((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "fork failed !!!"), &isCopy));
else if (pid == 0)
// 若监听文件所在文件夹不存在,创建
FILE *p_filesDir = fopen(APP_FILES_DIR, "r");
if (p_filesDir == NULL)
int filesDirRet = mkdir(APP_FILES_DIR, S_IRWXU | S_IRWXG | S_IXOTH);
if (filesDirRet == -1)
LOG_ERROR((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "mkdir failed !!!"), &isCopy));
// 若被监听文件不存在,创建文件
FILE *p_observedFile = fopen(APP_OBSERVED_FILE, "r");
if (p_observedFile == NULL)
p_observedFile = fopen(APP_OBSERVED_FILE, "w");
fclose(p_observedFile);
// 创建锁文件,通过检测加锁状态来保证只有一个卸载监听进程
int lockFileDescriptor = open(APP_LOCK_FILE, O_RDONLY);
if (lockFileDescriptor == -1)
lockFileDescriptor = open(APP_LOCK_FILE, O_CREAT);
int lockRet = flock(lockFileDescriptor, LOCK_EX | LOCK_NB);
if (lockRet == -1)
LOG_DEBUG((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "observed by another process"), &isCopy));
LOG_DEBUG((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "observed by child process"), &isCopy));
// 分配空间,以便读取event
void *p_buf = malloc(sizeof(struct inotify_event));
if (p_buf == NULL)
LOG_ERROR((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "malloc failed !!!"), &isCopy));
// 分配空间,以便打印mask
int maskStrLength = 7 + 10 + 1;// mask=0x占7字节,32位整形数最大为10位,转换为字符串占10字节,'\0'占1字节
char *p_maskStr = malloc(maskStrLength);
if (p_maskStr == NULL)
free(p_buf);
LOG_ERROR((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "malloc failed !!!"), &isCopy));
// 开始监听
LOG_DEBUG((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "start observe"), &isCopy));
int fileDescriptor = inotify_init();
if (fileDescriptor & 0)
free(p_buf);
free(p_maskStr);
LOG_ERROR((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "inotify_init failed !!!"), &isCopy));
// 添加被监听文件到监听列表
int watchDescriptor = inotify_add_watch(fileDescriptor, APP_OBSERVED_FILE, IN_ALL_EVENTS);
if (watchDescriptor & 0)
free(p_buf);
free(p_maskStr);
LOG_ERROR((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "inotify_add_watch failed !!!"), &isCopy));
// read会阻塞进程
size_t readBytes = read(fileDescriptor, p_buf, sizeof(struct inotify_event));
// 打印mask
snprintf(p_maskStr, maskStrLength, "mask=0x%x\0", ((struct inotify_event *) p_buf)-&mask);
LOG_DEBUG((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, p_maskStr), &isCopy));
// 若文件被删除,可能是已卸载,还需进一步判断app文件夹是否存在
if (IN_DELETE_SELF == ((struct inotify_event *) p_buf)-&mask)
FILE *p_appDir = fopen(APP_DIR, "r");
// 确认已卸载
if (p_appDir == NULL)
inotify_rm_watch(fileDescriptor, watchDescriptor);
// 未卸载,可能用户执行了"清除数据"
fclose(p_appDir);
// 重新创建被监听文件,并重新监听
FILE *p_observedFile = fopen(APP_OBSERVED_FILE, "w");
fclose(p_observedFile);
int watchDescriptor = inotify_add_watch(fileDescriptor, APP_OBSERVED_FILE, IN_ALL_EVENTS);
if (watchDescriptor & 0)
free(p_buf);
free(p_maskStr);
LOG_ERROR((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "inotify_add_watch failed !!!"), &isCopy));
// 释放资源
free(p_buf);
free(p_maskStr);
// 停止监听
LOG_DEBUG((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "stop observe"), &isCopy));
if (userSerial == NULL)
// 执行命令am start -a android.intent.action.VIEW -d $(url)
execlp("am", "am", "start", "-a", "android.intent.action.VIEW", "-d", "http://www.baidu.com", (char *)NULL);
// 执行命令am start --user userSerial -a android.intent.action.VIEW -d $(url)
execlp("am", "am", "start", "--user", (*env)-&GetStringUTFChars(env, userSerial, &isCopy), "-a", "android.intent.action.VIEW", "-d", "http://www.baidu.com", (char *)NULL);
// 执行命令失败log
LOG_ERROR((*env)-&GetStringUTFChars(env, tag, &isCopy)
, (*env)-&GetStringUTFChars(env, (*env)-&NewStringUTF(env, "exec AM command failed !!!"), &isCopy));
// 父进程直接退出,使子进程被init进程领养,以避免子进程僵死,同时返回子进程pid
210 #ifdef __cplusplus
212 #endif
注一:此次代码修复了评论中提到的一些bug,比如清除数据、插拔USB线、覆盖安装等操作引起程序误判卸载。注二:在同事指点下,针对任何情况导致重复监听的问题,都可以通过加文件锁来防止,这比ps并读取返回结果并过滤进程名的方法要好很多。
注三:安装在SD卡此卸载监听依然没有问题,但是如果用户将已在Internal SD卡安装好的应用移动到external SD卡,由于.c的161行未重新files文件夹和锁文件,应该会bug,代码都有,需要的自行修复此bug即可。
阅读(...) 评论()Posts - 16,
Articles - 0,
Comments - 402
安于现状,止于现状
15:20 by 周永恒, ... 阅读,
  用了三年多的WPF,开发了很多个WPF的项目,就我自己的经验,谈一谈如何学好WPF,当然,抛砖引玉,如果您有什么建议也希望不吝赐教。
  WPF,全名是Windows Presentation Foundation,是微软在.net3.0 WinFX中提出的。WPF是对Direct3D的托管封装,它的图形表现依赖于显卡。当然,作为一种更高层次的封装,对于硬件本身不支持的一些图形特效的硬实现,WPF提供了利用CPU进行计算的软实现,用以简化开发人员的工作。
  简单的介绍了一下WPF,这方面的资料也有很多。作于微软力推的技术,整个推行也符合微软一贯的风格。简单,易用,强大,外加几个创新概念的噱头。
  噱头一:声明式编程。从理论上讲,这个不算什么创新。Web界面声明式开发早已如火如荼了,这种界面层的声明式开发也是大势所趋。为了适应声明式编程,微软推出了XAML,一种扩展的XML语言,并且在.NET 3.0中加入了XAML的编译器和运行时解析器。XAML加上IDE强大的智能感知,确实大大方便了界面的描述,这点是值得肯定的。
  噱头二:紧接着,微软借XAML描绘了一副更为美好的图片,界面设计者和代码开发者可以并行的工作,两者通过XAML进行交互,实现设计和实现的分离。不得不说,这个想法非常打动人心。以往设计人员大多是通过photoshop编辑出来的图片来和开发人员进行交互的,需要开发人员根据图片的样式来进行转换,以生成实际的效果。既然有了这层转换,所以最终出来的效果和设计时总会有偏差,所以很多时候开发人员不得不忍受设计人员的抱怨。WPF的出现给开发人员看到了一线曙光,我只负责逻辑代码,UI你自己去搞,一结合就可以了,不错。可实际开发中,这里又出现了问题,UI的XAML部分能完全丢给设计人员么?
  这个话题展开可能有点长,微软提供了Expression Studio套装来支持用工具生成XAML。那么这套工具是否能够满足设计人员的需要呢?经过和很多设计人员和开发人员的配合,最常听到的话类似于这样。“这个没有Photoshop好用,会限制我的灵感”, “他们生成的XAML太糟糕了...”。确实,在同一项目中,设计人员使用Blend进行设计,开发人员用VS来开发代码逻辑,这个想法稍有理想化:&
  · 有些UI效果是很难或者不可以用XAML来描述的,需要手动编写效果。&
  · 大多数设计人员很难接受面向对象思维,包括对资源(Resource)的复用也不理想&
  · 用Blend生成的XAML代码并不高效,一种很简单的布局也可能被翻译成很冗长的XAML。
  在经历过这样不愉快的配合后,很多公司引入了一个integrator的概念。专门抽出一个比较有经验的开发人员,负责把设计人员提供的XAML代码整理成比较符合要求的XAML,并且在设计人员无法实现XAML的情况下,根据设计人员的需要来编写XAML或者手动编写代码。关于这方面,我的经验是,设计人员放弃Blend,使用Expression Design。Design工具还是比较符合设计人员的思维,当然,需要特别注意一些像素对齐方面的小问题。开发人员再通过设计人员提供的design文件转化到项目中。这里一般是用Blend打开工程,Expression系列复制粘贴是格式化到剪切板的,所以可以在design文件中选中某一个图形,点复制,再切到blend对应的父节点下点粘贴,适当修改一下转化过来的效果。
  作为一个矢量化图形工具,Expression Studio确实给我们提供了很多帮助,也可以达到设计人员同开发人员进行合作,不过,不像微软描述的那样自然。总的来说,还好,但是不够好。
  这里,要步入本篇文章的重点了,也是我很多时候听起来很无奈的事情。微软在宣传WPF时过于宣传XAML和工具的简易性了,造成很多刚接触WPF的朋友们会产生这样一副想法。WPF=XAML? 哦,类似HTML的玩意...
  这个是不对的,或者是不能这么说的。作为一款新的图形引擎,以Foundation作为后缀,代表了微软的野心。借助于托管平台的支持,微软寄希望WPF打破长久以来桌面开发和Web开发的壁垒。当然,由于需要.net3.0+版本的支持,XBAP已经逐渐被Silverlight所取替。在整个WPF的设计里,XAML(Markup)确实是他的亮点,也吸取了Web开发的精华。XAML对于帮助UI和实现的分离,有如如虎添翼。但XAML并不是WPF独有的,包括WF等其他技术也在使用它,如果你愿意,所有的UI你也可以完成用后台代码来实现。正是为了说明这个概念,Petzold在Application =
codes + markup 一书中一分为二,前半本书完全使用Code来实现的,后面才讲解了XAML以及在XAML中声明UI等。但这本书叫好不叫座,你有一定开发经验回头来看发现条条是路,非常经典,但你抱着这本书入门的话估计你可能就会一头雾水了。
  所以很多朋友来抱怨,WPF的学习太曲折了,上手很容易,可是深入一些就比较困难,经常碰到一些诡异困难的问题,最后只能推到不能做,不支持。复杂是由数量级别决定的,这里借LearnWPF的一些数据,来对比一下Asp.net, WinForm和WPF 类型以及类的数量:
ASP.NET 2.0
WinForms 2.0
1098 public
1551 classes
777 public types
1500 classes
1577&public
3592&classes
  当然,这个数字未必准确,也不能由此说明WPF相比Asp.net、WinForm,有多复杂。但是面对如此庞大的类库,想要做到一览众山小也是很困难的。想要搞定一个大家伙,我们就要把握它的脉络,所谓庖丁解牛,也需要知道在哪下刀。在正式谈如何学好WPF之前,我想和朋友们谈一下如何学好一门新技术。
  学习新技术有很多种途经,自学,培训等等。相对于我们来说,听说一门新技术,引起我们的兴趣,查询相关讲解的书籍(资料),边看书边动手写写Sample这种方式应该算最常见的。那么怎么样才算学好了,怎么样才算是学会了呢?在这里,解释下知识树的概念:
  这不是什么创造性的概念,也不想就此谈大。我感觉学习主要是两方面的事情,一方面是向内,一方面是向外。这棵所谓树的底层就是一些基础,当然,只是个举例,具体图中是不是这样的逻辑就不要见怪了。学习,就是一个不断丰富自己知识树的过程,我们一方面在努力的学习新东西,为它添枝加叶;另一方面,也会不停的思考,理清脉络。这里谈一下向内的概念,并不是没有学会底层一些的东西,上面的东西就全是空中楼阁了。很少有一门技术是仅仅从一方面发展来的,就是说它肯定不是只有一个根的。比方说没有学过IL,我并不认为.NET就无法学好,你可以从另外一个根,从相对高一些的抽象上来理解它。但是对底层,对这种关键的根,学一学它还是有助于我们理解的。这里我的感觉是,向内的探索是无止境的,向外的扩展是无限可能的。
  介绍了这个,接下来细谈一下如何学好一门新技术,也就是如何添砖加瓦。学习一门技术,就像新new了一个对象,你对它有了个大致了解,但它是游离在你的知识树之外的,你要做的很重要的一步就是把它连好。当然这层向内的连接不是一夕之功,可能会连错,可能会少连。我对学好的理解是要从外到内,再从内到外,就读书的例子谈一下这个过程:
  市面关于技术的书很多,名字也五花八门的,简单的整理一下,分为三类,就叫V1,V2,V3吧。&
· V1类,名字一般比较好认,类似30天学通XXX,一步一步XXX…没错,入门类书。这种书大致上都是以展示为主的,一个一个Sample,一步一步的带你过一下整个技术。大多数我们学习也都是从这开始的,倒杯茶水,打开电子书,再打开VS,敲敲代码,只要注意力集中些,基本不会跟不上。学完这一步,你应该对这门技术有了一定的了解,当然,你脑海中肯定不自觉的为这个向内连了很多线,当然不一定正确,不过这个新东东的创建已经有轮廓了,我们说,已经达到了从外的目的。&
· V2类,名字就比较乱了,其实意思差不多,只是用的词语不一样。这类有深入解析XXX,XXX本质论…这种书良莠不齐,有些明明是入门类书非要换个马甲。这类书主要是详细的讲一下书中的各个Feature, 来龙去脉,帮你更好的认识这门技术。如果你是带着问题去的,大多数也会帮你理清,书中也会顺带提一下这个技术的来源,帮你更好的把请脉络。这种书是可以看出作者的功力的,是不是真正达到了深入浅出。这个过程结束,我们说,已经达到了从外到内的目的。&
· V3类,如果你认真,踏实的走过了前两个阶段,我觉得在简历上写个精通也不为过。这里提到V3,其实名字上和V2也差不多。往内走的越深,越有种冲动想把这东西搞透,就像被强行注入了内力,虽然和体内脉络已经和谐了,不过总该自己试试怎么流转吧。这里谈到的就是由内向外的过程,第一本给我留下深刻印象的书就是侯捷老师的深入浅出MFC,在这本书中,侯捷老师从零开始,一步一步的构建起了整个类MFC的框架结构。书读两遍,如醍醐灌顶,痛快淋漓。如果朋友们有这种有思想,讲思想,有匠心的书也希望多多推荐,共同进步。
  回过头,就这个说一下WPF。WPF现在的书也有不少,入门的书我首推MSDN。其实我觉得作为入门类的书,微软的介绍就已经很好了,面面俱到,用词准确,Sample附带的也不错。再往下走,比如Sams.Windows.Presentation.Foundation.Unleashed或者Apress_Pro_WPF_Windows_Presentation_Foundation_in_NET_3_0也都非常不错。这里没有看到太深入的文章,偶有深入的也都是一笔带过,或者是直接用Reflector展示一下Code。
  接下来,谈一下WPF的一些Feature。因为工作关系,经常要给同事们培训讲解WPF,越来越发现,学好学懂未必能讲懂讲透,慢慢才体会到,这是一个插入点的问题。大家的水平参差不齐,也就是所谓的总口难调,那么讲解的插入点就决定了这个讲解能否有一个好的效果,这个插入点一定要尽可能多的插入到大家的知识树上去。最开始的插入点是大家比较熟悉的部分,那么往后的讲解就能一气通贯,反之就是一个接一个的概念,也就是最讨厌的用概念讲概念,搞得人一头雾水。
  首先说一下Dependency Property(DP)。这个也有很多人讲过了,包括我也经常和人讲起。讲它的储存,属性的继承,验证和强制值,反射和值储存的优先级等。那么为什么要有DP,它能带来什么好处呢?
  抛开DP,先说一下Property,属性,用来封装类的数据的。那么DP,翻译过来是依赖属性,也就是说类的属性要存在依赖,那么这层依赖是怎么来的呢。任意的一个DP,MSDN上的一个实践是这样的:
public&static&readonly&DependencyProperty IsSpinningProperty&=&DependencyProperty.Register("IsSpinning",&typeof(bool));&
public&bool&IsSpinning&
&&&&get&{&return&(bool)GetValue(IsSpinningProperty); }&
&&&&set&{ SetValue(IsSpinningProperty, value); }&
  单看IsSpinning,和传统的属性没什么区别,类型是bool型,有get,set方法。只不过内部的实现分别调用了GetValue和SetValue,这两个方法是DependecyObject(简称DO,是WPF中所有可视Visual的基类)暴露出来的,传入的参数是IsSpinningProperty。再看IsSpinningProperty,类型就是DependencyProperty,前面用了static readonly,一个单例模式,有DependencyProperty.Register,看起来像是往容器里注册。
  粗略一看,也就是如此。那么,它真正的创新、威力在哪里呢。抛开它精巧的设计不说,先看储存。DP中的数据也是存储在对象中的,每个DependencyObject内部维护了一个EffectiveValueEntry的数组,这个EffectiveValueEntry是一个结构,封装了一个DependencyProerty的各个状态值animatedValue(作动画),baseValue(原始值),coercedValue(强制值),expressionValue(表达式值)。我们使用DenpendencyObject.GetValue(IsSpinningProperty)时,就首先取到了该DP对应的EffectiveValueEntry,然后返回当前的Value。
  那么,它和传统属性的区别在哪里,为什么要搞出这样一个DP呢?第一,内存使用量。我们设计控件,不可避免的要设计很多控件的属性,高度,宽度等等,这样就会有大量(私有)字段的存在,一个继承树下来,低端的对象会无法避免的膨胀。而外部通过GetValue,SetValue暴露属性,内部维护这样一个EffectiveValueEntry的数组,顾名思义,只是维护了一个有效的、设置过值的列表,可以减少内存的使用量。第二,传统属性的局限性,这个有很多,包括一个属性只能设置一个值,不能得到变化的通知,无法为现有的类添加新的属性等等。
  这里谈谈DP的动态性,表现有二:可以为类A设置类B的属性;可以给类A添加新的属性。这都是传统属性所不具备的,那么是什么让DependencyObject具有这样的能力呢,就是这个DenpencyProperty的设计。在DP的设计中,对于单个的DP来说,是单例模式,也就是构造函数私有,我们调用DependencyProperty.Register或者DependencyProperty.RegisterAttached这些静态函数的时候,内部就会调用到私有的DP 的构造函数,构建出新的DP,并把这个DP加入到全局静态的一个HashTable中,键值就是根据传入时的名字和对象类型的hashcode取异或生成的。
  既然DependencyProperty是维护在一个全局的HashTable中的,那么具体到每个对象的属性又是怎么通过GetValue和SetValue来和DependencyProperty关联上的,并获得PropertyChangeCallback等等的能力呢。在一个DP的注册方法中,最多传递五个参数 :
public&static&DependencyProperty Register(string&name, Type propertyType, Type
ownerType, PropertyMetadata typeMetadata,
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&ValidateValueCallback
validateValueCallback);
其中第一和第三个参数就是用来确定HashTable中的键值,第二个参数确定了属性的类型,第四个参数是DP中的重点,定义了DP的属性元数据。在元数据中,定义了属性变化和强制值的Callback等。那么在一个SetValue的过程中,会出现哪些步骤呢:
取得该DP下对应这个DependencyObject的PropertyMetadata,这句可能听起来有些拗口。Metadata,按微软一般的命名规则,一般是用来描述对象自身数据的,那么一个DP是否只含有一个propertyMetadata呢?答案是不是,一个DP内部维护了一个比较高效的map,里面存储了多个propertyMetadata,也就是说DP和propertyMetadata是一对多的关系。这里是为什么呢,因为同一个DP可能会被用到不同的DependencyObject中去,对于每类DependencyObject,对这个DP的处理都有所不同,这个不同可以表现在默认值不同,properyMetadata里面的功能不同等等,所以在设计DP的时候设计了这样一个DP和propertyMetadata一对多的关系。
取得了该DP下对应真正干活的PropertyMetadata,下一步要真正的”SetValue”了。这个“value”就是要设置的值,设置之后要保存到我们前面提到的EffectiveValueEntry上,所以这里还要先取得这个DP对应的EffectiveValueEntry。在DependencyObject内部的EffectiveValueEntry的数组里面查找这个EffectiveValueEntry,有,取得;没有,新建,加入到数组中。
那么这个EffectiveValueEntry到底是用来干什么的,为什么需要这样一个结构体?如果你对WPF有一定了解,可能会听说WPF值储存的优先级,local value&style
trigger&template trigger&…。在一个EffectiveValueEntry中,定义了一个BaseValueSourceInternal,用来表示设置当前Value的优先级,当你用新的EffectiveValueEntry更新原有的EffectiveValueEntry时,如果新的EffectiveValueEntry中BaseValueSourceInternal高于老的,设置成功,否则,不予设置。
剩下的就是proertyMetadata了,当你使用类似如下的参数注册DP,
public&static&readonly&DependencyProperty CurrentReadingProperty&=&DependencyProperty.Register(&
&&&&"CurrentReading",&
&&&&typeof(double),&
&&&&typeof(Gauge),&
&&&&new&FrameworkPropertyMetadata(&
&&&&&&& Double.NaN,&
&&&&&&& FrameworkPropertyMetadataOptions.AffectsMeasure,&
&&&&&&&&new&PropertyChangedCallback(OnCurrentReadingChanged),&
&&&&&&&&new&CoerceValueCallback(CoerceCurrentReading)),&new&ValidateValueCallback(IsValidReading));
  当属性发生变化的时候,就会调用metadata中传入的委托函数。这个过程是这样的, DependencyObject中定义一个虚函数 :
protected&virtual&void
OnPropertyChanged(DependencyPropertyChangedEventArgs e)。
  当DP发生变化的时候就会先首先调用到这个OnPropertyChanged函数,然后如果metaData中设置了PropertyChangedCallback的委托,再调用委托函数。这里我们设置了FrameworkPropertyMetadataOptions.AffectsMeasure, 意思是这个DP变化的时候需要重新测量控件和子控件的Size。具体WPF的实现就是FrameworkElement这个类重载了父类DependencyObject的OnPropertyChanged方法,在这个方法中判断参数中的metadata是否是FrameworkPropertyMetadata,是否设置了
FrameworkPropertyMetadataOptions.AffectsMeasure这个标志位,如果有的话调用一下自身的InvalidateMeasure函数。
  简要的谈了一下DependencyProperty,除了微软那种自卖自夸,这个DependencyProperty究竟为我们设计实现带来了哪些好处呢?&
  1. 就是DP本身带有的PropertyChangeCallback等等,方便我们的使用。&
  2. DP的动态性,也可以叫做灵活性。举个例子,传统的属性,需要在设计类的时候设计好,你在汽车里拿飞机翅膀肯定是不可以的。可是DependencyObject,通过GetValue和SetValue来模仿属性,相对于每个DependencyObject内部有一个百宝囊,你可以随时往里放置数据,需要的时候又可以取出来。当然,前面的例子都是使用一个传统的CLR属性来封装了DP,看起来和传统属性一样需要声明,下面介绍一下WPF中很强大的Attached Property。
  再谈Attached Property之前,我打算和朋友们谈一个设计模式,结合项目实际,会更有助于分析DP,这就是MVVM(Mode-View-ViewModel)。关于这个模式,网上也有很多论述,也是我经常使用的一个模式。那么,它有什么特点,又有什么优缺点呢?先来看一个模式应用:
public&class&NameObject : INotifyPropertyChanged&
&&&&&&&&private&string&_name&=&"name1";&
&&&&&&&&public&string&Name&
&&&&&&& {&
&&&&&&&&&&&&get&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&&return&_&
&&&&&&&&&&& }&
&&&&&&&&&&&&set&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&
NotifyPropertyChanged("Name");&
&&&&&&&&&&& }&
&&&&&&& }&
&&&&&&&&private&void&NotifyPropertyChanged(string&name)&
&&&&&&& {&
&&&&&&&&&&&&if&(PropertyChanged&!=&null)&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&
PropertyChanged(this,&new&PropertyChangedEventArgs(name));&
&&&&&&&&&&& }&
&&&&&&& }&
&&&&&&&&public&event&PropertyChangedEventHandler
PropertyC&
&&&&public&class&NameObjectViewModel : INotifyPropertyChanged&
&&&&&&&&private&readonly&NameObject _&
&&&&&&&&public&NameObjectViewModel(NameObject
&&&&&&& {&
&&&&&&&&&&& _model&=&&
&&&&&&&&&&&
_model.PropertyChanged&+=&new&PropertyChangedEventHandler(_model_PropertyChanged);&
&&&&&&& }&
&&&&&&&&void&_model_PropertyChanged(object&sender, PropertyChangedEventArgs e)&
&&&&&&& {&
&&&&&&&&&&&
NotifyPropertyChanged(e.PropertyName);&
&&&&&&& }&
&&&&&&&&public&ICommand ChangeNameCommand&
&&&&&&& {&
&&&&&&&&&&&&get&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&&return&new&RelayCommand(&
&&&&&&&&&&&&&&&&&&&&&&&&&new&Action&object&((obj)&=&&
&&&&&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&&&&&&&&&
Name&=&"name2";&
&&&&&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&&&&&&new&Predicate&object&((obj)&=&&
&&&&&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&return&true;&
&&&&&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&& }&
&&&&&&& }&
&&&&&&&&public&string&Name&
&&&&&&& {&
&&&&&&&&&&&&get&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&&return&_model.N&
&&&&&&&&&&& }&
&&&&&&&&&&&&set&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&
_model.Name&=&&
&&&&&&&&&&& }&
&&&&&&& }&
&&&&&&&&private&void&NotifyPropertyChanged(string&name)&
&&&&&&& {&
&&&&&&&&&&&&if&(PropertyChanged&!=&null)&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&
PropertyChanged(this,&new&PropertyChangedEventArgs(name));&
&&&&&&&&&&& }&
&&&&&&& }&
&&&&&&&&public&event&PropertyChangedEventHandler
PropertyC&
public&class&RelayCommand : ICommand&
&&&&&&&&readonly&Action&object&&_&
&&&&&&&&readonly&Predicate&object&&_canE&
&&&&&&&&public&RelayCommand(Action&object&&execute, Predicate&object&&canExecute)&
&&&&&&& {&
&&&&&&&&&&& _execute&=&&
&&&&&&&&&&& _canExecute&=&canE&
&&&&&&& }&
&&&&&&&&public&bool&CanExecute(object&parameter)&
&&&&&&& {&
&&&&&&&&&&&&return&_canExecute&==&null&?&true&: _canExecute(parameter);&
&&&&&&& }&
&&&&&&&&public&event&EventHandler CanExecuteChanged&
&&&&&&& {&
&&&&&&&&&&& add {
CommandManager.RequerySuggested&+=& }&
&&&&&&&&&&& remove {
CommandManager.RequerySuggested&-=& }&
&&&&&&& }&
&&&&&&&&public&void&Execute(object&parameter)&
&&&&&&& {&
&&&&&&&&&&&
_execute(parameter);&
&&&&&&& }&
&&&&public&partial&class&Window1 : Window&
&&&&&&&&public&Window1()&
&&&&&&& {&
&&&&&&&&&&&
InitializeComponent();&
&&&&&&&&&&&&this.DataContext&=&new&NameObjectViewModel(new&NameObject());&
&&&&&&& }&
&Window&x:Class="WpfApplication7.Window1"&
&&&&&&& xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"&
&&&&&&& xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"&
&&&&&&& Title="Window1"&Height="300"&Width="300"&&
&&&&&Grid&&
&&&&&&&&&TextBlock&Margin="29,45,129,0"&Name="textBlock1"&Height="21"&VerticalAlignment="Top"&
&&&&&&&&&&&&&&&&&&
Text="{Binding Path=Name}"/&&
&&&&&&&&&Button&Height="23"&Margin="76,0,128,46"&Name="button1"&VerticalAlignment="Bottom"&
&&&&&&&&&&&&&&&
Command="{Binding Path=ChangeNameCommand}"&Rename&/Button&&
&&&&&/Grid&&
类的关系如图所示:
  这里NameObject -&&Model,NameObjectViewModel
-&&ViewModel,Window1 -&&View。我们知道,在通常的Model-View世界中,无论MVC也好,MVP也好,包括我们现在提到的MVVM,它的Model和View的功能都类似,Model是用来封装核心数据,逻辑与功能计算的模型,View是视图,具体可以对应到窗体(控件)等。那么View的功能主要有,把Model的数据显示出来,响应用户的操作,修改Model,剩下Controller或Presenter的功能就是要组织Model和View之间的关系,整理一下Model-View世界中的需求点,大致有:&
  1. 为View提供数据,如何把Model中的数据提供给View。&
  2. Model中的数据发生变化后,View如何更新视图。&
  3. 根据不同的情况为Model选择不同的View。&
  4. 如何响应用户的操作,鼠标点击或者一些其他的事件,来修改Model。
  所谓时势造英雄,那么WPF为MVVM打造了一个什么“时势“呢。&
1. FrameworkElement类中定义了属性DataContext(数据上下文),所有继承于FrameworkElement的类都可以使用这个数据上下文,我们在XAML中的使用类似Text=”{Binding Path=Name}”的时候,隐藏的含义就是从这个控件的DataContext(即NameObjectViewModel)中取它的Name属性。相当于通过DataContext,使View和Model中存在了一种松耦合的关系。&
2. WPF强大的Binding(绑定)机制,可以在Model发生变化的时候自动更新UI,前提是Model要实现INotifyPropertyChanged接口,在Model数据发生变化的时候,发出ProperyChaned事件,View接收到这个事件后,会自动更新绑定的UI。当然,使用WPF的DenpendencyProperty,发生变化时,View也会更新,而且相对于使用INotifyPropertyChanged,更为高效。&
3. DataTemplate和DataTemplateSelector,即数据模板和数据模板选择器。可以根据Model的类型或者自定义选择逻辑来选择不同的View。&
4. 使用WPF内置的Command机制,相对来说,我们对事件更为熟悉。比如一个Button被点击,一个Click事件会被唤起,我们可以注册Button的Click事件以处理我们的逻辑。在这个例子里,我使用的是Command="{Binding Path=ChangeNameCommand}",这里的ChangeNameCommand就是DataContext(即NameObjectViewModel)中的属性,这个属性返回的类型是ICommand。在构建这个Command的时候,设置了CanExecute和Execute的逻辑,那么这个ICommand什么时候会调用,Button Click的时候会调用么?是的,WPF内置中提供了ICommandSource接口,实现了这个接口的控件就有了触发Command的可能,当然具体的触发逻辑要自己来控制。Button的基类ButtonBase就实现了这个接口,并且在它的虚函数OnClick中触发了这个Command,当然,这个Command已经被我们绑定到ChangeNameCommand上去了,所以Button被点击的时候我们构建ChangeNameCommand传入的委托得以被调用。
  正是借助了WPF强大的支持,MVVM自从提出,就获得了好评。那么总结一下,它真正的亮点在哪里呢?&
1. 使代码更加干净,我没使用简洁这个词,因为使用这个模式后,代码量无疑是增加了,但View和Model之间的逻辑更清晰了。MVVM致力打造一种纯净UI的效果,这里的纯净指后台的xaml.cs,如果你编写过WPF的代码,可能会出现过后台xaml.cs代码急剧膨胀的情况。尤其是主window的后台代码,动则上千行的代码,整个window内的控件事件代码以及逻辑代码混在一起,看的让人发恶。&
2. 可测试性。更新UI的时候,只要Model更改后发出了propertyChanged事件,绑定的UI就会更新;对于Command,只要我们点击了Button,Command就会调用,其实是借助了WPF内置的绑定和Command机制。如果在这层意思上来说,那么我们就可以直接编写测试代码,在ViewModel上测试。如果修改数据后得到了propertyChanged事件,且值已经更新,说明逻辑正确;手动去触发Command,模拟用户的操作,查看结果等等。就是把UnitTest也看成一个View,这样Model-ViewModel-View和Model-ViewModel-UnitTest就是等价的。&
3. 使用Attached Behavior解耦事件,对于前面的例子,Button的点击,我们已经尝试了使用Command而不是传统的Event来修改数据。是的,相对与注册事件并使用,无疑使用Command使我们的代码更“和谐“,如果可以把控件的全部事件都用Command来提供有多好,当然,控件的Command最多一个,Event却很多,MouseMove、MouseLeave等等,指望控件暴露出那么多Command来提供绑定不太现实。这里提供了一个Attached Behavior模式,目的很简单,就是要注册控件的Event,然后在Event触发时时候调用Command。类似的Sample如下:
&&&&&&&&public&static&DependencyProperty
PreviewMouseLeftButtonDownCommandProperty&=&DependencyProperty.RegisterAttached(&
&&&&&&&&&&&&&&&&"PreviewMouseLeftButtonDown",&
&&&&&&&&&&&&&&&&typeof(ICommand),&
&&&&&&&&&&&&&&&&typeof(AttachHelper),&
&&&&&&&&&&&&&&&&new&FrameworkPropertyMetadata(null,&new&PropertyChangedCallback(AttachHelper.PreviewMouseLeftButtonDownChanged)));&
&&&&&&&&public&static&void&SetPreviewMouseLeftButtonDown(DependencyObject
target, ICommand value)&
&&&&&&& {&
&&&&&&&&&&&
target.SetValue(AttachHelper.PreviewMouseLeftButtonDownCommandProperty, value);&
&&&&&&& }&
&&&&&&&&public&static&ICommand
GetPreviewMouseLeftButtonDown(DependencyObject target)&
&&&&&&& {&
&&&&&&&&&&&&return&(ICommand)target.GetValue(AttachHelper.PreviewMouseLeftButtonDownCommandProperty);&
&&&&&&& }&
&&&&&&&&private&static&void&PreviewMouseLeftButtonDownChanged(DependencyObject
target, DependencyPropertyChangedEventArgs e)&
&&&&&&& {&
&&&&&&&&&&&
FrameworkElement element&=&target&as&FrameworkE&
&&&&&&&&&&&&if&(element&!=&null)&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&&if&((e.NewValue&!=&null)&&&&(e.OldValue&==&null))&
&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&
element.PreviewMouseLeftButtonDown&+=&element_PreviewMouseLeftButtonD&
&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&else&if&((e.NewValue&==&null)&&&&(e.OldValue&!=&null))&
&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&
element.PreviewMouseLeftButtonDown&-=&element_PreviewMouseLeftButtonD&
&&&&&&&&&&&&&&&
&&&&&&&&&&& }&
&&&&&&& }&
&&&&&&&&private&static&void&element_PreviewMouseLeftButtonDown(object&sender, MouseButtonEventArgs e)&
&&&&&&& {&
&&&&&&&&&&&
FrameworkElement element&=&(FrameworkElement)&
&&&&&&&&&&& ICommand
command&=&(ICommand)element.GetValue(AttachHelper.PreviewMouseLeftButtonDownCommandProperty);&
&&&&&&&&&&&
command.Execute(sender);&
  这里用到了DependencyProperty.RegisterAttached这个AttachedProperty,关于这个模式,留到下面去讲,这段代码的主要意思就是注册控件的PreviewMouseLeftButtonDown事件,在事件唤起时调用AttachedProperty传入的Command。
  那么是不是这个模式真的就这么完美呢,当然不是,MVVM配上WPF自然是如鱼得水,不过它也有很多不足,或者不适合使用的场合:
1. 这个模式需要Model-ViewModel,在大量数据的时候为每个Model都生成这样一个ViewModel显然有些过。ViewModel之所以得名,因为它要把Model的属性逐一封装,来给View提供绑定。
2. Command的使用,前面提到过,实现ICommandSource的接口才具备提供Command的能力,那是不是WPF的内置控件都实现了这样的接口呢?答案是不是,很少,只有像Button,MenuItem等少数控件实现了这一接口,像我们比较常用ComboBoxItem就没有实现这一接口。接口没实现,我们想使用Command的绑定更是无从谈起了。这个时候我们要使用Command,就不得不自己写一个ComboxBoxCommandItem继承于ComboBoxItem,然后自己实现ICommandSource,并且在Click的时候触发Command的执行了。看起来这个想法不算太好,那不是要自己写很多控件,目的就是为了用Command,也太为了模式而模式了。但像Expression Blend,它就是定义了很多控件,目的就是为了使用Command,说起来也奇怪,自己设计的控件,用起来自己还需要封装,这么多个版本也不添加,这个有点说不过去了。
3. 纯UI,就是在控件后台的cs代码中除了构造函数最多只有一行,this.DataContext =
设置一下数据上下文。当然,我目前的项目代码大都是这样的,还是那句话,不要为了模式而模式。那么多的控件event,不管是使用Attached模式还是用一些奇技淫巧用反射来构建出Command,都没什么必要。目前我的做法就是定义一个LoadedCommand,在这个Command中引用界面上的UI元素,ViewModel拿到这个UI元素后在ViewModel中注册控件事件并处理。还是第一个优点,这么做只是为了让代码更干净,逻辑更清晰,如果都把各个控件事件代码都写在一个xaml.cs中看起来比较混乱。
  谈过了MVVM,接下来重点谈AttachedProperty,这个是很好很强大的feature,也是WPF真正让我有不一样感觉的地方。前面简单谈过了DependencyProperty的原理,很多初接触WPF的朋友们都会觉得DP很绕,主要是被它的名字和我们的第一直觉所欺骗。如果我们定义了一个DP,MyNameProperty,类型是string的。那么在DependencyObject上,我谈过了有个百宝囊,就是EffectiveValueEntry数组,它内部最终储存MyName的值也是string,这个DependencyProperty(即MyNameProperty)是个静态属性,是在你设置读取这个string的时候起作用的,如何起作用是通过它注册时定义的propertyMetadata决定的。
  简单来说就是DependencyObject可以使用DependencyProperty,但两者没有从属关系,你在一个DependencyObject中定义了一个DP,在另一个DependencyObject也可以使用这个DP,你在另一个DependencyObject中写一个CLR属性使用GetValue和SetValue封装这个DP是一样的。唯一DependencyProperty和DependencyObject有关联的地方就是你注册的时候,DP保存在全局静态DP的Hashtable里的键值是通过注册时的名字和这个DependencyObject的类型的hashcode取异或生成的。但这个键值也可以不唯一,DP提供了一个AddOwner方法,你可以为这个DP在全局静态DP中提供一个新键值,当然,这两个键值指向同一个DP。
  既然如此,那么为什么有DependencyProperty.Register和DependencyProperty.RegisterAttached两种方法注册DP呢。既然DP只是一个引子,通过GetValue和SetValue,传入DependencyObject就可以取得存储在其中EffectiveValueEntry里面的值,这两个不是一样的么?恩,原理上是一个,区别在于,前面提到,一个DependencyProperty里面会有多个propertyMetadata,比如说Button定义了一个DP,我们又写了一个CustomButton,继承于Button。我们在CustomButton的静态函数中调用了前面DP的OverrideMetadata函数,DP的OverrideMetadata会涉及到Merge操作,它要把新旧的propertyMetadata合二为一成一个,作为新的propertyMetadata,而这个overrideMetadata过程需要调用时传入的类型必须是DependencyObject的。DependencyProperty.Register和DependencyProperty.RegisterAttached的区别是前者内部调用了OverrideMetadata而后者没有,也就意味着Rigister方法只能是DependencyObject调用,而后者可以在任何对象中注册。
  就这一个区别么?恩,还有的,默认的封装方法,Register是使用CLR属性来封装的,RegisterAttached是用静态的Get,Set来封装的。Designer反射的时候,遇到静态的封装会智能感知成类似Grid.Column=这样的方式。这个就类似于非要说菜刀有两大功能,一是砍菜,二是砍人。你要感到纳闷,不是因为菜刀有刀刃么?它会和你解释,不同不同,砍菜进行了优化,你可以用手握着,砍人犯法,最好飞出去…
  那么为什么微软要把这个注册过程分为Register和RegisterAttached两类呢?就是为了强调Attach这个概念,这个过程就是DependencyObject(相当于银行金库,有很多箱子)通过DependencyProperty(相当于开箱子的钥匙)取得自己箱子里的财宝一样,当然这些所有的钥匙有人统一管理(全局的HashTable),你来拿钥匙的时候还要刁难一下你(通过钥匙上的附带的propertyMetadata)检查一下你的身份啦,你存取东西要发出一些通知啦等等。这个Attach,翻译过来叫附加,所谓的AttachedProperty(附加属性),就是说人家可以随时新配一把钥匙来你这新开一个箱子,或者拿一把旧钥匙来你这新开个箱子,谁让你箱子多呢?
  强调了这么多,只是为了说明一点,这个Attach的能力不是因为你注册了RegisterAttached才具备的,而是DependencyProperty本身设计就支持的。那么这个设计能为我们开发程序带来哪些好处呢?
  从继承和接口实现来说,人们初期阶段有些乱用继承,后来出现了接口,只有明确有IS-A语义的才用继承,能力方面的用接口来支持。比如飞行,那么一般会定义到一个IFlyable的接口,我们实现这个接口以获得飞行的能力。那么这个能力的获得要在类的设计阶段继承接口来获得,那么作为一个已经成熟的人,我是大雄,我要飞,怎么办?
AttachedProperty来救你。代码如下:
&&&&public&partial&class&Window1 : Window&
&&&&&&&&public&Window1()&
&&&&&&& {&
&&&&&&&&&&&
InitializeComponent();&
&&&&&&&&&&&&this.DataContext&=&new&DragonFlyViewModel();&
&&&&&&& }&
&&&&public&interface&IFlyHandler&
&&&&&&&&void&Fly();&
&&&&public&class&DragonFlyViewModel : IFlyHandler&
&&&&&&&&public&void&Fly()&
&&&&&&& {&
&&&&&&&&&&&
MessageBox.Show("送你个竹蜻蜓,飞吧!");&
&&&&&&& }&
&&&&public&class&FlyHelper&
&&&&&&&&public&static&readonly&DependencyProperty
FlyHandlerProperty&=&
&&&&&&&&&&&
DependencyProperty.RegisterAttached("FlyHandler",&typeof(IFlyHandler),&typeof(FlyHelper),&
&&&&&&&&&&&&&&&&new&FrameworkPropertyMetadata(null,&new&PropertyChangedCallback(OnFlyHandlerPropertyChanged)));&
&&&&&&&&public&static&IFlyHandler
GetFlyHandler(DependencyObject d)&
&&&&&&& {&
&&&&&&&&&&&&return&(IFlyHandler)d.GetValue(FlyHandlerProperty);&
&&&&&&& }&
&&&&&&&&public&static&void&SetFlyHandler(DependencyObject d,
IFlyHandler value)&
&&&&&&& {&
&&&&&&&&&&&
d.SetValue(FlyHandlerProperty, value);&
&&&&&&& }&
&&&&&&&&public&static&void&OnFlyHandlerPropertyChanged(DependencyObject
target, DependencyPropertyChangedEventArgs e)&
&&&&&&& {&
&&&&&&&&&&&
FrameworkElement element&=&target&as&FrameworkE&
&&&&&&&&&&&&if&(element&!=&null)&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&
IFlyHandler flyHander&=&e.NewValue&as&IFlyH&
&&&&&&&&&&&&&&&
element.MouseLeftButtonDown&+=&new&MouseButtonEventHandler((sender, ex)&=&&
&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&if&(flyHander&!=&null)&
&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&&&&
flyHander.Fly();&
&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&
&&&&&&&&&&& }&
&&&&&&& }&
&Window&x:Class="WpfApplication7.Window1"&
&&&&&&& xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"&
&&&&&&& xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"&
&&&&&&& xmlns:local="clr-namespace:WpfApplication7"&
&&&&&&& Title="Window1"&Height="300"&Width="300"&&
&&&&&Grid&&
&&&&&&&&&Label&Margin="72,58,88,113"&Name="label1"&Background="Yellow"&
&&&&&&&&&&&&&&
local:FlyHelper.FlyHandler="{Binding}"&我叫大雄我不会飞&/Label&&
&&&&&/Grid&&
  这是一个最简单的模式应用,当然,还不是很完美,不过已经可以起飞了。我们在FlyHelper中使用DependencyProperty.RegisterAttached注册了一个AttachedProperty,在OnFlyHandlerPropertyChanged中订阅了element的MouseLeftButtonDown事件,事件处理就是”起飞”。这里定义了一个IFlyHandler的接口,使用ViewModel模式,ViewModel去实现这个接口,然后使用local:FlyHelper.FlyHandler="{Binding}"绑定,这里{Binding}没有写path,默认绑定到DataContext本身,也就是DragonFlyViewModel上。
  你说什么?你要去追小静?那一定要帮你。你往脑门上点一下,看,是不是会飞了?怎么样,戴着竹蜻蜓的感觉很好吧,^_^。大雄欣喜若狂,连声感谢。不过,这么欺骗一个老实人的感觉不太好,实话实说了吧。你真是有宝物不会用啊,你胸前挂着那是啥?小口袋?百宝囊?那是机器猫的口袋,汽车大炮时空飞船,什么掏不出来啊。哦,你嫌竹蜻蜓太慢了?你等等。
&&&&public&partial&class&Window1 : Window&
&&&&&&&&public&Window1()&
&&&&&&& {&
&&&&&&&&&&&
InitializeComponent();&
&&&&&&&&&&&&this.DataContext&=&new&DragonFlyViewModel();&
&&&&&&& }&
&&&&&&&&private&void&button1_Click(object&sender, RoutedEventArgs e)&
&&&&&&& {&
&&&&&&&&&&&&this.DataContext&=&new&FighterViewModel();&
&&&&&&& }&
&&&&public&interface&IFlyHandler&
&&&&&&&&void&Fly();&
&&&&public&class&DragonFlyViewModel : IFlyHandler&
&&&&&&&&public&void&Fly()&
&&&&&&& {&
&&&&&&&&&&&
MessageBox.Show("送你个竹蜻蜓,飞吧!");&
&&&&&&& }&
&&&&public&class&FighterViewModel : IFlyHandler&
&&&&&&&&public&void&Fly()&
&&&&&&& {&
&&&&&&&&&&&
MessageBox.Show("送你驾战斗机,为了小静,冲吧!");&
&&&&&&& }&
&&&&public&class&FlyHelper&
&&&&&&&&private&IFlyHandler _flyH&
&&&&&&&&public&FlyHelper(IFlyHandler handler,
FrameworkElement element)&
&&&&&&& {&
&&&&&&&&&&& _flyHandler&=&&
&&&&&&&&&&&
element.MouseLeftButtonDown&+=&new&MouseButtonEventHandler(element_MouseLeftButtonDown);&
&&&&&&& }&
&&&&&&&&void&element_MouseLeftButtonDown(object&sender, MouseButtonEventArgs e)&
&&&&&&& {&
&&&&&&&&&&&&if&(_flyHandler&!=&null)&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&
_flyHandler.Fly();&
&&&&&&&&&&& }&
&&&&&&& }&
&&&&&&&&private&void&UpdateFlyHandler(IFlyHandler
&&&&&&& {&
&&&&&&&&&&& _flyHandler&=&&
&&&&&&& }&
&&&&&&&&#region&FlyHelper&
&&&&&&&&public&static&readonly&DependencyProperty FlyHelperProperty&=&
&&&&&&&&&&& DependencyProperty.RegisterAttached("FlyHelper",&typeof(FlyHelper),&typeof(FlyHelper),&
&&&&&&&&&&&&&&&&new&FrameworkPropertyMetadata(null));&
&&&&&&&&public&static&FlyHelper
GetFlyHelper(DependencyObject d)&
&&&&&&& {&
&&&&&&&&&&&&return&(FlyHelper)d.GetValue(FlyHelperProperty);&
&&&&&&& }&
&&&&&&&&public&static&void&SetFlyHelper(DependencyObject d,
FlyHelper value)&
&&&&&&& {&
&&&&&&&&&&&
d.SetValue(FlyHelperProperty, value);&
&&&&&&& }&
&&&&&&&&#endregion&
&&&&&&&&#region&FlyHandler&
&&&&&&&&public&static&readonly&DependencyProperty
FlyHandlerProperty&=&
&&&&&&&&&&&
DependencyProperty.RegisterAttached("FlyHandler",&typeof(IFlyHandler),&typeof(FlyHelper),&
&&&&&&&&&&&&&&&&new&FrameworkPropertyMetadata(null,&new&PropertyChangedCallback(OnFlyHandlerPropertyChanged)));&
&&&&&&&&public&static&IFlyHandler
GetFlyHandler(DependencyObject d)&
&&&&&&& {&
&&&&&&&&&&&&return&(IFlyHandler)d.GetValue(FlyHandlerProperty);&
&&&&&&& }&
&&&&&&&&public&static&void&SetFlyHandler(DependencyObject d,
IFlyHandler value)&
&&&&&&& {&
&&&&&&&&&&&
d.SetValue(FlyHandlerProperty, value);&
&&&&&&& }&
&&&&&&&&public&static&void&OnFlyHandlerPropertyChanged(DependencyObject
target, DependencyPropertyChangedEventArgs e)&
&&&&&&& {&
&&&&&&&&&&&
FrameworkElement element&=&target&as&FrameworkE&
&&&&&&&&&&&&if&(element&!=&null)&
&&&&&&&&&&& {&
&&&&&&&&&&&&&&&
FlyHelper helper&=&(FlyHelper)element.GetValue(FlyHelperProperty);&
&&&&&&&&&&&&&&&&if&(helper&==&null)&
&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&
IFlyHandler handler&=&e.NewValue&as&IFlyH&
&&&&&&&&&&&&&&&&&&&&if&(handler&!=&null)&
&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&&&&
helper&=&new&FlyHelper(handler, element);&
&&&&&&&&&&&&&&&&&&&&&&&
element.SetValue(FlyHelperProperty, helper);&
&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&else&
&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&
IFlyHandler handler2&=&e.NewValue&as&IFlyH&
&&&&&&&&&&&&&&&&&&&&//handler2 may be null, this usually happened when this.DataContext =
null, release IFlyHandler.&
&&&&&&&&&&&&&&&&&&&
helper.UpdateFlyHandler(handler2);&
&&&&&&&&&&&&&&&
&&&&&&&&&&& }&
&&&&&&& }&
&&&&&&&&#endregion&&&&&&&
  这里就是一个完整的Attached模式,这里我添加了一个新的AttachedProperty,类型是FlyHelper,当local:FlyHelper.FlyHandler="{Binding}"绑定值发生变化时,判断传入的这个DependencyObject内是否有FlyHelper对象,没有,构造一个,然后塞入到这个DependencyObject中去;如果有,则更新FlyHelper内持有的IFlyHandler对象。这个Attached模式的好处在于,这个辅助的Helper对象是在运行时构造的,构造之后塞入到UI对象(DependencyObject)中去,仅是UI对象持有这个引用,UI对象被释放后这个Helper对象也被释放。FlyHelper对象用于控制何时”起飞”,至于怎么飞则依赖于IFlyHandler这个接口,这层依赖是在绑定时注入的,而这个绑定最终是运用了DataContext这个数据上下文,和MVVM模式搭配的很完美。这也就是MVVM模式中强调的,也就是唯一的依赖,设置控件的DataContext。
  回顾一下,作于例子中的Label,是不具备“飞行“能力的。这种不具备具体说就是不知道什么时候触发动作,也不知道触发了之后该干什么。通过一个Attach模式使它具备了这个能力,而且可以随时更新动作。简直达到了一种让你飞,你就飞的境界,值得为它喝彩。
  鉴于这种动态添加控件的能力,这种模式也被称为Attached Behavior。在Blend 3中,也加入了Behaviors的支持,很多通用的能力,都可以用Behavior来把它抽出来,比如缩放,DragDrop等等。我没有具体研究过Blend的Behavior,应该也是这种方法或演变吧。在实际项目中,我也大量使用了MVVM和Attached Behavior,配上CommandBinding,Unit Test,脚本化UIAutomation,以及Prism等框架,对一些比较大型的项目,还是很有帮助的。
  顺着DP这条线讲下来,还是蛮有味道的。当然,WPF中还有一些比较新的概念,包括逻辑树和视觉树,路由事件,Style和Template等等。其实纵看WPF,还是有几条主线的,包括刚才讲到的DP,Threading Model与Dispatcher,视觉树和依赖它产生的路由,Template和Style等等。那么就回到开头了,如何学好WPF呢?
  其实写这篇文章之前,我是经常带着这疑问的。现在新技术推出的很快,虽说没什么技术是凭空产生,都是逐渐衍变而来的。可是真学下去也要花成本,那怎么样才是学好了呢,怎么能融入到项目呢?后来总结了下,我需要了解这么一些情况:&
  1. 这门技术是否成熟,前景如何?&
  2. 摆脱宣传和炒作,这门技术的优缺点在哪里?&
  3. 希望看到一些对这门技术有整体把握的文章,可以不讲细节,主要是帮我理出一个轮廓,最好和我的知识树连一连。&
  4. 有没有应用的成功案例。&
 5. 最重要的,呵呵,有没有可以下载的电子书。
  关于WPF,现在讲解的书籍和资料已经蛮多了。随着.NET Framework的升级,包括性能以及辅助工具的支持也越来越好了。但不得不说,WPF学习的时间成本还是很大的。WPF的设计很重,带着很浓的设计痕迹,查看WPF的源码,也许你会有种很熟悉的感觉。这种熟悉不是那种流畅美妙之感,到有种到了项目后期,拿着性能测试去优化,拿着Bug报告乱堵窟窿的感觉。
祝大家WPF愉快。…^_^
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。}

我要回帖

更多关于 设备验证的全过程 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信