[WPF] Same DataContext Container

本文疑似本博主原创,转载请注明是你自己写的。


首先来制造一个架空的需求:有时候需要只修改一个对象中指定的属性,在界面上使用一个复选框来决定是否要应用其修改。

为了方便说明,先假定对应的Model是介样的:

    public class TestModel : ViewModelBase
    {
        private string a;
        public string A
        {
            get { return this.a; }
            set
            {
                this.a = value;
                this.OnPropertyChanged("A");
            }
        }

        private DateTime b = DateTime.Now;
        public DateTime B
        {
            get { return this.b; }
            set
            {
                this.b = value;
                this.OnPropertyChanged("B");
            }
        }

        private int c;
        public int C
        {
            get { return this.c; }
            set
            {
                this.c = value;
                this.OnPropertyChanged("C");
            }
        }
    }

其中ViewModelBase这个wpf的必备大坑,做()wpf的应该都只到,而且版本各不相同,这是我个人的版本:

    public class ViewModelBase : System.ComponentModel.INotifyPropertyChanged
    {
        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string name)
        {
            try
            {
                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, 
                        new System.ComponentModel.PropertyChangedEventArgs(name));
                }
                else
                {
                    Console.WriteLine("Property \"{0}\" not bound to UI.", name);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("INotifyPropertyChanged error: {0}", e.Message);
            }
        }
    }

一般猿类会在界面上这么搞:

首先挨个把对应的控件堆上去
堆控件

堆控件效果

在ViewModel里也挨个写上去
挨个写上去

然后在AChecked那里加上对AEnabled的控制
加控制

好了,大功告成。

然后后面还有100个属性在等着……


这时候,在普通猿类继续之前的步骤当着码农的时候,文艺猿类不高兴了,这特么的有啥意思,于是文艺猿类就想,这么多长得差不多的控件一个个去组装多没意思,直接放进用户控件里不就行了,于是乎

于是乎

此外还需要获取复选框是否被选中以及文本框里填上的值,没问题,再写几个依赖项属性不就得了

再写几个

具体内容是这样的(: 不能折叠不关我事)

    public partial class CheckedView : UserControl
    {
        public CheckedView()
        {
            InitializeComponent();
        }

        #region IsChecked
        public bool? IsChecked
        {
            get { return (bool?)GetValue(IsCheckedProperty); }
            set { SetValue(IsCheckedProperty, value); }
        }

        public static readonly DependencyProperty IsCheckedProperty =
            DependencyProperty.Register("IsChecked", typeof(bool?), typeof(CheckedView),
             new PropertyMetadata(null, onIsCheckedPropertyChanged));

        private static void onIsCheckedPropertyChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {
            (d as CheckedView).checkbox.IsChecked = (bool?)e.NewValue;
        }
        #endregion IsChecked

        #region Title
        public string Title
        {
            get { return (string)GetValue(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }

        public static readonly DependencyProperty TitleProperty =
            DependencyProperty.Register("Title", typeof(string), typeof(CheckedView),
                new PropertyMetadata(null, onTitlePropertyChanged));

        private static void onTitlePropertyChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {
            (d as CheckedView).checkbox.Content = e.NewValue;
        }
        #endregion Title

        #region Value
        public string Value
        {
            get { return (string)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register("Value", typeof(string), typeof(CheckedView), 
                new PropertyMetadata(null, onValuePropertyChanged));

        private static void onValuePropertyChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {
            (d as CheckedView).textbox.Text = (string)e.NewValue;
        }
        #endregion Value
}

然后放到界面上,顺便测试下刚加的几个依赖项属性
然后放到界面上

顺便测试下

大功又告成了。


这时普通猿类一看,顿时觉得啊哈啊啊哈哈,这搞了半天,只弄出来一个String类型的,其他类型的全都得搞一个对应的控件出来。

然后文艺猿类用一种炒鸡腻害且实用的办法解决了,普通猿类顿时觉得心服口服。


: 当然上面并不是在说我啊)

我并不是文艺猿类,猜不到现实中文艺猿类接下来是用多么文艺的方式解决这个问题的。之所以这么说,那是因为接下来轮到我这个二笔猿类折腾了。


首先看下需求,发现实际上要用到的东西有三个:名称、值、是否选中。

所以写出一个包含了这三样东西的类(: 瞎命名の術)
三神器之类

这里的Name指的是属性的名称,不一定是界面上要显示的东西,毕竟很多时候两者不一样

然后再折腾一个类
再折腾之类

然后界面那边,因为输入值的控件需要根据具体情况自行加进去,所以使用ContentControl来实现这一功能。

界面上
界面上

唯一变化的就是把固定的文本框换成了支持动态内容的控件。

然后依赖项属性中的Title和IsChecked不变,移除掉之前的Value,再加一个表示ContentControl的Content的属性

由于UserControl也有ContentProperty,所以需要覆盖掉UserControl对应的属性

Q: 啥是覆盖?A: new啊)

覆盖Content


Q: 接下来该干嘛?
A: 接下来扯点别的。


既然操作对象是类而不是属性,那么就得考虑使用一个集合来装这些属性,到时候就可以遍历这个集合来取到需要的数据了。

那么目标就是写一个集合类来装Model中所有的属性对应的CheckedProperty类。

一个类里面的属性名称一定都是不同的(: 我猜),那么使用一个字典类型(Dictionary<string, CheckedProperty>)来储存再好不过了。

在这个集合类的构造函数中传入一个Model,然后将其中的属性转换为CheckedProperty并储存,之后再实现遍历接口即可。

一步步看(: 这里的partial只是为了方便说明)

首先是传入Model和保存数据的部分

    public partial class CheckedPropertyCollection
    {
        public CheckedPropertyCollection(object obj)
        {
            if (obj != null)
            {
                //如果是原始类型比如int啥的,拒绝
                if (!obj.GetType().IsPrimitive)
                {
                    //无脑遍历属性
                    var props = obj.GetType().GetProperties();
                    foreach (var prop in props)
                    {
                        //不知道怎么在已知泛型类型的情况下初始化泛型类的小伙伴看过来了
                        //对应的,调用泛型方法的方法也差不多能猜到了
                        Type constructedType = typeof(CheckedProperty<>)
                            .MakeGenericType(prop.PropertyType);
                        CheckedProperty item = (CheckedProperty)Activator
                            .CreateInstance(constructedType);
                        this.add(item);
                    }
                }
                else
                {
                    throw new ArgumentOutOfRangeException("no support promitive types");
                }
            }
            else
            {
                throw new ArgumentNullException("obj");
            }
        }

        private Dictionary<string, CheckedProperty> map = new Dictionary<string, CheckedProperty>();

        private void add(CheckedProperty item)
        {
            try
            {
                if (!this.map.ContainsKey(item.Name))
                {
                    //没有就添加
                    this.map.Add(item.Name, item);
                }
                else
                {
                    //有就替换。当然强迫症才会觉得这种情况会出现
                    this.map.Remove(item.Name);
                    this.map.Add(item.Name, item);
                }
            }
            catch
            {
                throw;
            }
        }
    }

: 我英语有八十亿词汇量呢,请不要在评论区吐槽)
: 其实我知道浏览量都是百度等网站的爬虫爬过的痕迹)

然后实现遍历接口。

首先在类内部实现一个遍历器

public partial class CheckedPropertyCollection
{
    private sealed class Enumerator : IEnumerator<CheckedProperty>
    {
        public Enumerator(ICollection<CheckedProperty> source)
        {
            this.list = new List<CheckedProperty>();
            foreach (CheckedProperty item in source)
            {
                this.list.Add(item);
            }
            this.index = -1;
            this.count = this.list.Count;
        }

        private IList<CheckedProperty> list;
        private int index;
        private int count;

        public CheckedProperty Current
        {
            get
            {
                return this.list[this.index];
            }
        }

        public void Dispose()
        {
            if (this.list != null)
            {
                this.list.Clear();
                this.list = null;
            }
        }

        object IEnumerator.Current
        {
            get { return this.Current; }
        }

        public bool MoveNext()
        {
            if (this.count > 0)
            {
                if (++this.index < this.count)
                {
                    return true;
                }
                this.index--;
            }
            return false;
        }

        public void Reset()
        {
            if (this.count > 0)
            {
                this.index = 0;
            }
        }
    }
}

然后实现IEnumerable<T>接口

    public class CheckedPropertyCollection : IEnumerable<CheckedProperty>
    {
        public IEnumerator<CheckedProperty> GetEnumerator()
        {
            return new Enumerator(this.map.Values);
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return new Enumerator(this.map.Values);
        }
    }

这时就有人跳出来说我在误导别人了

假想中跳出来的读者:

上面两块那么多行代码完全没意义啊,直接这样

    public class CheckedPropertyCollection : IEnumerable<CheckedProperty>
    {
        public IEnumerator<CheckedProperty> GetEnumerator()
        {
            return this.map.Values.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.map.Values.GetEnumerator();
        }
    }

不就行了,也比你自己写的更权威更加令人信服。

嗯,确实是这样,我无力狡辩。(: 其实我也是偶然发现被自己坑了)

: 其实我又出现了有人会来看的幻觉)


然后呢?扯了这么多还是不知道我要干啥,我得好好想想自己到底要干啥 。

——来自我的第二人格大队副队长的第一百八十一个影分身的第三人格小队的第五个人格

这个集合类目的是要在界面绑定中替换掉原来的Model(假想读者: 啥时候说的?)。

那么说起界面绑定,wpf的Binding在形式上有两种

一种就是平时经常用的XXX.XXX.XXX这样,例如
XXX.XXX.XXX

还有一种也许不太常用(Q: 谁说的 A: 我猜的),那就是使用索引来绑定。

Q: 啥是索引?
A: 比如在字典中,写俩方括号来读写值的就是索引

// Summary:
// Gets or sets the value associated with the specified key.
//
public TValue this[TKey key] { get; set; }

接着这个来说,比如在对应的ViewModel中有个字典
有个词典

在界面上
在界面上

运行

运行结果

现在思路应该清晰一些了,用这个集合类来代替Model进行绑定。

具体绑定到CheckedView(假想读者: 这是啥?)上的是CheckedProperty这个类,所以再调整一下CheckedView
CheckedView

接下来在ViewModel中加上TestModel(假想读者: 这又是啥?)字段
model字段

以及对应的集合类属性
对应的集合类属性

然后在ViewModel的构造函数中初始化(: 因为俺不知道直接写在字段后面会不会按顺序初始化)
初始化

然后在界面上做手脚
做手脚

这么一来,当界面上的文本框内容变化时,集合类中对应的CheckedProperty中的Value属性就会跟着变化。


搞定了这些后,还需要个方法来验证其结果是不是和自己的预期一致,所以再加个按钮和TextBlock(: 咋翻译啊)

测试

ViewModel中

        private string testResult;
        public string TestResult
        {
            get { return this.testResult; }
            set
            {
                if (this.testResult != value)
                {
                    this.testResult = value;
                    this.OnPropertyChanged("TestResult");
                }
            }
        }

        private RelayCommand test;
        public RelayCommand Test
        {
            get
            {
                if (this.test == null)
                {
                    this.test = new RelayCommand(this.onTest);
                }
                return this.test;
            }
            set
            {
                this.test = value;
            }
        }

        private void onTest(object obj)
        {
            StringBuilder sb = new StringBuilder();
            try
            {
                var items = this.props
                    .Where(item => item.Checked)
                    .Select(item => new { item.Name, item.Value })
                    .ToArray();
                if (items.Length > 0)
                {
                    foreach (var item in items)
                    {
                        sb.AppendFormat("{0} - {1}\r\n", item.Name, item.Value);
                    }
                }
            }
            finally
            {
                if (sb.Length > 0)
                {
                    this.TestResult = sb.ToString();
                }
            }
        }

结果

测试结果

接下来就可以加上TestModel中剩下的两个属性来测了。


如果要在CheckedProperty中的Value改变的时候将这个修改同时应用到原始的Model类对象中对应的属性上呢?

首先,要给Model的属性赋值需要两个数据:Model类对象以及指定的属性

需要两个数据

在构造函数中传入引用
传入引用

再修改Value属性的读写方法
修改读写方法

然后在泛型子类(假想读者: 哪来的?)中做出对应的修改
修改子类

别忘了修改要用到的地方(假想读者: 哪里哪里?)
别忘了


好了,折腾了这么久,一般猿类早已经测试完界面并开始做下一个任务了。

而我还在给同事“推销(十块钱)”我这个东东,而且还没人要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值