65.9K
CodeProject 正在变化。 阅读更多。
Home

绑定布尔属性值的反值

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (4投票s)

2009年10月22日

CPOL

2分钟阅读

viewsIcon

33845

downloadIcon

144

是否曾希望能够绑定到“禁用”而不是“启用”?

引言

通常,用户界面代码会因为操作状态的事件处理程序而变得混乱,例如,当点击按钮 A 时,禁用按钮 B 等。

数据绑定是简化用户界面逻辑的好方法。本文假定您熟悉 .NET 数据绑定和 WinForms。(如果您不熟悉这个概念,可以在 CodeProject 或 MSDN 上找到相关信息——这是一种优雅的技术,我将其用于许多目的)。

背景

任何熟悉 WinForms 开发的人都肯定见过管理 UI 元素 Enabled 状态的代码。

传统方法包括仔细的事件处理程序和属性初始化。

  public Form1()
  {
     InitializeComponent();
     
     button1.Enabled = true;
     button2.Enabled = false;
     panel1.Enabled = false;
  }
  
  private void button1_Click(object sender, EventArgs e)
  {
     button1.Enabled = false;
     button2.Enabled = true;
     panel1.Enabled = true;
  }
  
  private void button2_Click(object sender, EventArgs e)
  {
     button1.Enabled = true;
     button2.Enabled = false;
     panel1.Enabled = false;
  }

这可以通过绑定来简化,例如:

  public Form1()
  {
     InitializeComponent();
     
     button1.Enabled = true;
     button2.Enabled = false;
     panel1.DataBindings.Add(new Binding("Enabled", button2, "Enabled"));
  }
  
  private void button1_Click(object sender, EventArgs e)
  {
     button1.Enabled = false;
     button2.Enabled = true;
  }
  
  private void button2_Click(object sender, EventArgs e)
  {
     button1.Enabled = true;
     button2.Enabled = false;
  }

不幸的是,由于按钮 1 和 2 是反向相关的,即当按钮 1 启用时,按钮 2 禁用,因此我们无法添加传统的绑定。这种情况发生的频率比你想象的要高,例如,连接和断开按钮的行为就完全是这样。本文定义的 InvertedBinding 结构为此问题提供了一个无缝的解决方案。

Using the Code

附带的项目是一个针对 .NET 2.0 Framework 的 VS 2008 解决方案,但相关的代码包含在内。只需获取 InvertedBinding 类即可。

使用示例

  public Form1()
  {
     InitializeComponent();
     
     button2.DataBindings.Add(InvertedBinding.Create(button1, "Enabled"));
     panel1.DataBindings.Add(new Binding("Enabled", button2, "Enabled"));
  }
  
  private void button1_Click(object sender, EventArgs e)
  {
     button1.Enabled = false;
  }
  
  private void button2_Click(object sender, EventArgs e)
  {
     button1.Enabled = true;
  }

现在,整个 UI 启用状态都由 button1.Enabled 驱动。 也不需要在设计器或代码中设置初始值。

实现
using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows.Forms;

namespace InverseBinding
{
   public class InvertedBinding : INotifyPropertyChanged, IDisposable
   {
      #region Private Fields
      private readonly object _dataSource;
      private readonly EventInfo _changedEvent;
      private readonly MethodInfo _getAccessor;
      private readonly MethodInfo _setAccessor;
      #endregion

      #region Constructors
      protected InvertedBinding
		(object dataSource, string dataSourceBoundPropertyName)
      {
         if ((dataSource == null) || (dataSourceBoundPropertyName == null))
         {
            throw new ArgumentNullException();
         }

         _dataSource = dataSource;

         EventInfo propertyChangedEvent = 
            _dataSource.GetType().GetEvent(string.Format("{0}Changed", 
					dataSourceBoundPropertyName));
         if ((propertyChangedEvent != null) && (typeof(EventHandler).
		IsAssignableFrom(propertyChangedEvent.EventHandlerType)))
         {
            _changedEvent = propertyChangedEvent;
            _changedEvent.AddEventHandler(_dataSource, 
		new EventHandler(OnDataSourcePropertyChanged));
         }

         PropertyInfo dataSourceBoundProperty = 
            _dataSource.GetType().GetProperty(dataSourceBoundPropertyName);
         if (dataSourceBoundProperty == null)
         {
            throw new MissingMemberException(string.Format(
                         	"Could not find property '{0}.{1}'",
                         	_dataSource.GetType().FullName, 
			dataSourceBoundPropertyName));
         }

         _getAccessor = dataSourceBoundProperty.GetGetMethod();
         if (_getAccessor == null)
         {
            throw new MissingMethodException(string.Format(
                     "No get accessor for '{0}'", dataSourceBoundProperty.Name));
         }
         if (!typeof(bool).IsAssignableFrom(_getAccessor.ReturnType))
         {
            throw new ArgumentException(
               string.Format(
                  "Class only works on boolean properties, 
		'{0}' is not of type bool", 
		dataSourceBoundProperty.Name));
         }

         _setAccessor = dataSourceBoundProperty.GetSetMethod();
      }
      #endregion

      public static Binding Create(object dataSource, string propertyName)
      {
         return new Binding(propertyName, 
		new InvertedBinding(dataSource, propertyName), "InvertedProperty");
      }

      public bool InvertedProperty
      {
         get
         {
            return !GetDataBoundValue();
         }
         set
         {
            if (_setAccessor == null)
            {
               // nothing to do since no one will get notified.
               return;
            }

            bool curVal = InvertedProperty;
            
            // a little bit of trickery here, we only want to change 
            // the value if IS the same
            // rather than the conventional if it's different
            if (curVal == value)
            {
               _setAccessor.Invoke(_dataSource, new object[] { !value });
               if (PropertyChanged != null)
               {
                  PropertyChanged(this, 
			new PropertyChangedEventArgs("InvertedProperty"));
               }
            }
         }
      }

      #region INotifyPropertyChanged Members
      public event PropertyChangedEventHandler PropertyChanged;
      #endregion

      #region IDisposable Members
      public void Dispose()
      {
         if (_changedEvent != null)
         {
            _changedEvent.RemoveEventHandler
		(_dataSource, new EventHandler(OnDataSourcePropertyChanged));
         }
      }
      #endregion

      private void OnDataSourcePropertyChanged(object sender, EventArgs e)
      {
         // refresh our property (which may trigger our PropertyChanged event)
         InvertedProperty = !GetDataBoundValue();
      }

      private bool GetDataBoundValue()
      {
         return (bool) _getAccessor.Invoke(_dataSource, null);
      }
   }
}

关注点

实现 INotifyPropertyChanged 非常重要,以便框架(例如 ButtonPanel)在绑定属性更改时更新其状态。

代码展示了许多反射示例,包括动态事件发现和注册。 这种事情对我来说已经不再令人兴奋了,因为这对我来说已经习以为常,但初学者可能会觉得很有趣。:)

可以将此代码调整为针对 .NET 3.0,这将允许使用 lambda 消除魔术字符串

历史

  1. 初始修订版
  2. 小更新(定制异常消息,添加 XML 文档)
© . All rights reserved.