[廚餘回收] 當 Nullable Value Types 與 Binding 在 Windows Forms 中相遇

Nullable Value Types 允許實值型別可為 Null,這點加大了實值型別應用的彈性,不過卻也帶來了一些副作用,當 Nullable Value Types 成為 Binding 的 Data Source 時,我們會發現它不再像以前一樣正常 Work 了。

先說明一下我的實驗環境,我有一個 MyViewModel 的類別,裡面有一個型別為 int? 的 Id 屬性,跟 UI 上一個 TextBox 的 Text 屬性做雙向綁定,結果就像上面那張圖一樣,綁定無法正常運作。

這個問題的原因我們從 Binding 的 Source Code 當中就可以瞭解了,控制項的屬性值與 Data Source 之間的轉換是靠 Convert.ChangeType() 方法在處理的,轉換的來源物件必須是實作了 IConvertible 介面的物件。

當我在 TextBox 上輸入數字之後,此時轉換的來源物件是 TextBox.Text 屬性,而轉換的目標物件則是 MyViewModel.Id 屬性,TextBox.Text 屬性的型別是 string,string 是有實作 IConvertible 介面的物件沒錯,但是 IConvertible 介面卻沒有定義轉換成 int? 的方法,這個會引發 InvalidCastException

所以,我將 TextBox.Text 綁定到 MyViewModel.Id 這件事情,實際上在 Binding 內部是引發了 InvalidCastException,只是 Binding 內部把這個 Exception 給處理掉了,而且還會將 TextBox.Text 改回前一個有效綁定值,因此才會看到像文章開頭的那張圖那樣,輸入的文字都消失了。

面對 IConvertible 無法轉換的型別,我們得自己客製化轉換邏輯,Binding 類別有一個 Parse 事件可以註冊,在裡面我們就可以客製化我們自己想要的轉換邏輯,除此之外,還需要將 FormattingEnabled 設為 true,這是因為 Nullable Value Types 在 HasValue 為 true 的情況之下,boxing 之後會還原回對應的實值型別,從而觸發 Binding 內部執行 Convert.ChangeType() 方法的條件,再度引發 InvalidCastException,經過這樣調整完之後,綁定的效果就正常了。

完整原始碼

public partial class Form1 : Form
{
    private readonly MyViewModel myViewModel;

    public Form1()
    {
        this.InitializeComponent();

        this.myViewModel = new MyViewModel();

        var binding = new Binding("Text", this.myViewModel, nameof(MyViewModel.Id), true);

        binding.Parse += (sender, args) =>
            {
                args.Value = int.TryParse((string)args.Value, out var value) ? value : default(int?);
            };

        this.textBox1.DataBindings.Add(binding);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        this.myViewModel.Id = new Random(Guid.NewGuid().GetHashCode()).Next(100);
    }
}

public class MyViewModel : INotifyPropertyChanged
{
    private int? id;

    public event PropertyChangedEventHandler PropertyChanged;

    public int? Id
    {
        get => this.id;
        set
        {
            if (value == this.id) return;

            this.id = value;
            this.OnPropertyChanged();

            Console.WriteLine($"{nameof(MyViewModel.Id)} change to {value}.");
        }
    }

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學