• 使用扩展方法打造一套简单的WPF/SL绑定机制


    在昨天的博文《WPF/Silverlight的数据绑定设计的真糟糕》中,分析了三大应用场景下WPF/SL下绑定的缺陷。我的应用中有95%以上(甚至99%以上)的绑定都属于那三种应用场景,因此无法接受WPF/SL繁琐的绑定机制。骂不是目的,想办法解决问题是黑客精神的所在。知道缺陷之后,就可以来弥补。刚才花了两个小时时间用扩展方法写了一套新的WPF/SL绑定机制。

    本文只是一份简单的草案和思路探索,可以证明了这种机制的可行性。最近时间紧张,暂时还无法给出一份完备的解决方案,有兴趣的朋友可以沿着本文的思路写一套更完善的扩展方法出来。

    一、朴素的绑定需求

    为什么要骂?因为有大量“朴素”的绑定需求,WPF/SL做的不好。比如下图:

    界面

    这个界面有四个编辑状态:None 代表不能编辑,DrawRegion 代表在图片上画框,DrawFrontPoint 代表在图片上画前景点,DrawBackgroundPoint 代表在图片上画背景点。用枚举 EditMode 来代表编辑状态,将 EditMode 存储在属性 EditModeValue 中:

    public enum EditMode
    {
        None,
        DrawRegion,
        DrawFrontPoint,
        DrawBackgroundPoint
    }

    一个很简单的需求就是把 EditModeValue 和右边的几个按钮的状态关联起来,比如说,当 EditModeValue  为 DrawRegion 时,“绘制分割区域”按钮呈现按下状态(IsEnabled 为false),其它按钮类似。当按下某个按钮时,自动将 EditModeValue 改为某种状态,

    数据绑定是解决这个问题的最佳方案。然而,WPF/SL下的数据绑定实在太不给力了,写这种数据绑定很繁琐,让人敬而远之。

    二、新的方案

    下面用反射+扩展方法来写一套更简洁的数据绑定方案。直接上代码,代码不足一百行,实现了绑定属性链的功能(没做完全的测试,可能有错误,谨慎使用):

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Text;
    using System.Reflection;

    namespace Orc.Util
    {
        public static class BindingHelper
        {
            internal class PropertyChangedCallback
            {
                internal INotifyPropertyChanged Caller { get; set; }
                internal String PropertyName { get; set; }
                internal Action Callback { get; set; }
                internal INotifyPropertyChanged Src { get; set; }
                internal String Path { get; set; }
                internal List<PropertyChangedCallback> CallbackList { get; set; }
                internal void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
                {
                    if (e.PropertyName == PropertyName)
                    {
                        // 移除事件,避免副作用
                        if (CallbackList != null)
                        {
                            foreach (PropertyChangedCallback item in CallbackList)
                            {
                                if (item.Caller != null)
                                    item.Caller.PropertyChanged -= item.OnPropertyChanged;
                            }
                            CallbackList.Clear();
                        }

                        // 回调
                        Callback();

                        // 添加事件
                        AddCallback(Src, Callback, Path);
                    }
                }

                internal static void AddCallback(INotifyPropertyChanged src, Action callback, String path)
                {
                    if (src == null || callback == null || String.IsNullOrEmpty(path)) return;

                    List<PropertyChangedCallback> list = new List<PropertyChangedCallback>();
                    INotifyPropertyChanged current = src;
                    Type type = src.GetType();
                    String[] pathes = path.Split('.');
                    foreach (String item in pathes)
                    {
                        if (current == null) return;

                        String name = item.Trim();
                        if (String.IsNullOrEmpty(name)) break;

                        PropertyChangedCallback c = new PropertyChangedCallback();
                        c.Callback = callback;
                        c.Caller = current;
                        c.Src = src;
                        c.PropertyName = name;
                        c.CallbackList = list;
                        c.Path = path;
                        current.PropertyChanged += c.OnPropertyChanged;
                        list.Add(c);

                        PropertyInfo pi = type.GetProperty(name);
                        if (pi == null) break;

                        Object p = pi.GetValue(current, null);
                        INotifyPropertyChanged np = p as INotifyPropertyChanged;
                        if (np == null) break;

                        current = np;
                        type = np.GetType();
                    }
                }
            }

            public static void SetBindingChain(this Object dst, INotifyPropertyChanged src, String path, Action callback)
            {
                if (src == null || callback == null || String.IsNullOrEmpty(path)) return;
                PropertyChangedCallback.AddCallback(src, callback, path);
            }
        }
    }

    SetBindingChain 就是用于绑定的扩展方法,src 是绑定源,path是绑定属性链,语法和属性调用语法类似(A.B.C.D),可以通过IDE提示写完后复制进去,callback是当属性发生变化后的回调事件,可以是lambda表达式。使用最好是在窗体Load时或某个事件时做个批量绑定。

    也可以在这思路基础上写出多属性链绑定及多对象绑定扩展方法,本文就不写了,下面是原型:

            public static void SetBindingChain(this Object dst, INotifyPropertyChanged src, String[] pathes, Action callback)
            {
                throw new NotImplementedException();
            }

            public static void SetBindingChain(this Object dst, INotifyPropertyChanged src1, String path1, INotifyPropertyChanged src2, String path2, Action callback)
            {
                throw new NotImplementedException();
            }

            public static void SetBindingChain(this Object dst, INotifyPropertyChanged src1, String[] pathes1, INotifyPropertyChanged src2, String[] pathes2, Action callback)
            {
                throw new NotImplementedException();
            }

            public static void SetBindingChain(this Object dst, INotifyPropertyChanged src1, String path1, INotifyPropertyChanged src2, String path2, INotifyPropertyChanged src3, String path3, Action callback)
            {
                throw new NotImplementedException();
            }

            public static void SetBindingChain(this Object dst, INotifyPropertyChanged src1, String[] pathes1, INotifyPropertyChanged src2, String[] pathes2, INotifyPropertyChanged src3, String[] pathes3, Action callback)
            {
                throw new NotImplementedException();
            }

            public static void SetBindingChain(this Object dst, INotifyPropertyChanged src1, String path1, INotifyPropertyChanged src2, String path2, INotifyPropertyChanged src3, String path3, INotifyPropertyChanged src4, String path4, Action callback)
            {
                throw new NotImplementedException();
            }

            public static void SetBindingChain(this Object dst, INotifyPropertyChanged src1, String[] pathes1, INotifyPropertyChanged src2, String[] pathes2, INotifyPropertyChanged src3, String[] pathes3, INotifyPropertyChanged src4, String[] pathes4, Action callback)
            {
                throw new NotImplementedException();
            }

    三、使用新方案进行数据绑定

    EditModeValue属性:

    private EditMode m_editModeValue = EditMode.None;

    public EditMode EditModeValue
    {
        get { return m_editModeValue; }
        set {
            if (m_editModeValue == value) return;
            m_editModeValue = value;
            NotifyPropertyChanged("EditModeValue");
        }
    }

    单向绑定:

    this.SetBindingChain(this, "EditModeValue",
       () =>
       {
           btnDrawSegRegion.IsEnabled = EditModeValue != EditMode.DrawRegion;
           btnDrawFPoints.IsEnabled = EditModeValue != EditMode.DrawFrontPoint;
           btnDrawBPoints.IsEnabled = EditModeValue != EditMode.DrawBackgroundPoint;
       });

    然后是按下按钮时状态改变:

    private void btnDrawSegRegion_Click(object sender, RoutedEventArgs e)
    {
        EditModeValue = EditMode.DrawRegion;
    }

    private void btnDrawFPoints_Click(object sender, RoutedEventArgs e)
    {
        EditModeValue = EditMode.DrawFrontPoint;
    }

    private void btnDrawBPoints_Click(object sender, RoutedEventArgs e)
    {
        EditModeValue = EditMode.DrawBackgroundPoint;
    }

    搞定!

    四、分析

    使用扩展方法 SetBindingChain 可以简化 《WPF/Silverlight的数据绑定设计的真糟糕》 中所提到的前两种场景,使用方便了很多,且无学习成本,能够满足大部分的绑定需求。使用体验靠近了Flex的绑定方案,也有些区别:

    (1)绑定写在后台,没有前台直观,同时,也不方便解耦;

    (2)Flex是自动分析绑定链,这里还要手动输入绑定链,是一个比较大的遗憾;

    尽管如此,相对于又难学又难用的WPF/SL源生绑定,要好太多了,简洁、明了、不用学、不用记、柔性、适合大部分应用场景。本文只是对属性链的绑定,如有其它需求,如对索引器的绑定等等还请自行扩展。

    至于第三种场景,目前还没好的解决办法。

    五、其它

    喜欢简洁明了的解决方案,比如说上面界面中的打开图像功能,我的代码是:

    private void btnOpen_Click(object sender, RoutedEventArgs e)
    {
        this.OpenImageFile((String path) => {
            tbImgPath.Text = path;
            EditModeValue = EditMode.None;
        });
    }

    非常简单明了,不用拖个 OpenFileDialog 出来,也不用记忆 filter 的语法。当然,在背后有个扩展方法在默默的提供服务:

    public static void OpenImageFile(this Window element, Action<String> callbackOnFilePath, String filter = "图像文件|*.bmp;*.jpg;*.gif;*.png")
    {
        String filePath;
        OpenFileDialog dlg = new OpenFileDialog();

        dlg.Filter = filter;
        dlg.FileOk += (object sender, CancelEventArgs e) =>
        {
            filePath = dlg.FileName;
            if(callbackOnFilePath != null)
                callbackOnFilePath(filePath);
        };
        dlg.ShowDialog();
    }

  • 相关阅读:
    C#中的Dictionary字典类介绍
    SQL server 2008r2 file is corrupt
    web service接口 wsdl和asmx有什么区别
    ascx
    C++: C++函数声明的时候后面加const
    C++三种野指针及应对/内存泄露
    C++构造和析构的顺序
    atan2()如何转换为角度
    C++11左值引用和右值引用
    C++ STL详解
  • 原文地址:https://www.cnblogs.com/xiaotie/p/1955051.html
Copyright © 2020-2023  润新知