File: System\Windows\Forms\BindingTests.cs
Web Access
Project: src\src\System.Windows.Forms\tests\UnitTests\System.Windows.Forms.Tests.csproj (System.Windows.Forms.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Security;
 
namespace System.Windows.Forms.Tests;
 
// NB: doesn't require thread affinity
public class BindingTests
{
    public static IEnumerable<object[]> Ctor_String_Object_String_TestData()
    {
        yield return new object[] { null, null, null };
        yield return new object[] { string.Empty, new(), string.Empty };
        yield return new object[] { "propertyName", new(), "dataMember" };
        yield return new object[] { "propertyName", new(), "dataMember.subDataMember" };
    }
 
    [Theory]
    [MemberData(nameof(Ctor_String_Object_String_TestData))]
    public void Binding_Ctor_String_Object_String(string propertyName, object dataSource, string dataMember)
    {
        Binding binding = new(propertyName, dataSource, dataMember);
        Assert.Null(binding.BindableComponent);
        Assert.Null(binding.BindingManagerBase);
        Assert.Equal(new BindingMemberInfo(dataMember), binding.BindingMemberInfo);
        Assert.Null(binding.Control);
        Assert.Equal(ControlUpdateMode.OnPropertyChanged, binding.ControlUpdateMode);
        Assert.Same(dataSource, binding.DataSource);
        Assert.Same(DBNull.Value, binding.DataSourceNullValue);
        Assert.Equal(DataSourceUpdateMode.OnValidation, binding.DataSourceUpdateMode);
        Assert.Null(binding.FormatInfo);
        Assert.Empty(binding.FormatString);
        Assert.False(binding.FormattingEnabled);
        Assert.False(binding.IsBinding);
        Assert.Null(binding.NullValue);
        Assert.Same(propertyName, binding.PropertyName);
    }
 
    public static IEnumerable<object[]> Ctor_String_Object_String_Bool_TestData()
    {
        yield return new object[] { null, null, null, true };
        yield return new object[] { string.Empty, new(), string.Empty, true };
        yield return new object[] { "propertyName", new(), "dataMember", false };
        yield return new object[] { "propertyName", new(), "dataMember.subDataMember", false };
    }
 
    [Theory]
    [MemberData(nameof(Ctor_String_Object_String_Bool_TestData))]
    public void Binding_Ctor_String_Object_String_Bool(string propertyName, object dataSource, string dataMember, bool formattingEnabled)
    {
        Binding binding = new(propertyName, dataSource, dataMember, formattingEnabled);
        Assert.Null(binding.BindableComponent);
        Assert.Null(binding.BindingManagerBase);
        Assert.Equal(new BindingMemberInfo(dataMember), binding.BindingMemberInfo);
        Assert.Null(binding.Control);
        Assert.Equal(ControlUpdateMode.OnPropertyChanged, binding.ControlUpdateMode);
        Assert.Same(dataSource, binding.DataSource);
        Assert.Same(DBNull.Value, binding.DataSourceNullValue);
        Assert.Equal(DataSourceUpdateMode.OnValidation, binding.DataSourceUpdateMode);
        Assert.Null(binding.FormatInfo);
        Assert.Empty(binding.FormatString);
        Assert.Equal(formattingEnabled, binding.FormattingEnabled);
        Assert.False(binding.IsBinding);
        Assert.Null(binding.NullValue);
        Assert.Same(propertyName, binding.PropertyName);
    }
 
    public static IEnumerable<object[]> Ctor_String_Object_String_Bool_DataSourceUpdateMode_TestData()
    {
        yield return new object[] { null, null, null, true, DataSourceUpdateMode.OnValidation };
        yield return new object[] { string.Empty, new(), string.Empty, true, DataSourceUpdateMode.OnValidation - 1 };
        yield return new object[] { "propertyName", new(), "dataMember", false, DataSourceUpdateMode.Never };
        yield return new object[] { "propertyName", new(), "dataMember.subDataMember", false, DataSourceUpdateMode.Never + 1 };
    }
 
    [Theory]
    [MemberData(nameof(Ctor_String_Object_String_Bool_DataSourceUpdateMode_TestData))]
    public void Binding_Ctor_String_Object_String_Bool_DataSourceUpdateMode(string propertyName, object dataSource, string dataMember, bool formattingEnabled, DataSourceUpdateMode dataSourceUpdateMode)
    {
        Binding binding = new(propertyName, dataSource, dataMember, formattingEnabled, dataSourceUpdateMode);
        Assert.Null(binding.BindableComponent);
        Assert.Null(binding.BindingManagerBase);
        Assert.Equal(new BindingMemberInfo(dataMember), binding.BindingMemberInfo);
        Assert.Null(binding.Control);
        Assert.Equal(ControlUpdateMode.OnPropertyChanged, binding.ControlUpdateMode);
        Assert.Same(dataSource, binding.DataSource);
        Assert.Same(DBNull.Value, binding.DataSourceNullValue);
        Assert.Equal(dataSourceUpdateMode, binding.DataSourceUpdateMode);
        Assert.Null(binding.FormatInfo);
        Assert.Empty(binding.FormatString);
        Assert.Equal(formattingEnabled, binding.FormattingEnabled);
        Assert.False(binding.IsBinding);
        Assert.Null(binding.NullValue);
        Assert.Same(propertyName, binding.PropertyName);
    }
 
    public static IEnumerable<object[]> Ctor_String_Object_String_Bool_DataSourceUpdateMode_Object_TestData()
    {
        yield return new object[] { null, null, null, true, DataSourceUpdateMode.OnValidation, null };
        yield return new object[] { string.Empty, new(), string.Empty, true, DataSourceUpdateMode.OnValidation - 1, DBNull.Value };
        yield return new object[] { "propertyName", new(), "dataMember", false, DataSourceUpdateMode.Never, new() };
        yield return new object[] { "propertyName", new(), "dataMember.subDataMember", false, DataSourceUpdateMode.Never + 1, new() };
    }
 
    [Theory]
    [MemberData(nameof(Ctor_String_Object_String_Bool_DataSourceUpdateMode_Object_TestData))]
    public void Binding_Ctor_String_Object_String_Bool_DataSourceUpdateMode_Object(string propertyName, object dataSource, string dataMember, bool formattingEnabled, DataSourceUpdateMode dataSourceUpdateMode, object nullValue)
    {
        Binding binding = new(propertyName, dataSource, dataMember, formattingEnabled, dataSourceUpdateMode, nullValue);
        Assert.Null(binding.BindableComponent);
        Assert.Null(binding.BindingManagerBase);
        Assert.Equal(new BindingMemberInfo(dataMember), binding.BindingMemberInfo);
        Assert.Null(binding.Control);
        Assert.Equal(ControlUpdateMode.OnPropertyChanged, binding.ControlUpdateMode);
        Assert.Same(dataSource, binding.DataSource);
        Assert.Same(DBNull.Value, binding.DataSourceNullValue);
        Assert.Equal(dataSourceUpdateMode, binding.DataSourceUpdateMode);
        Assert.Null(binding.FormatInfo);
        Assert.Empty(binding.FormatString);
        Assert.Equal(formattingEnabled, binding.FormattingEnabled);
        Assert.False(binding.IsBinding);
        Assert.Same(nullValue, binding.NullValue);
        Assert.Same(propertyName, binding.PropertyName);
    }
 
    public static IEnumerable<object[]> Ctor_String_Object_String_Bool_DataSourceUpdateMode_Object_String_TestData()
    {
        yield return new object[] { null, null, null, true, DataSourceUpdateMode.OnValidation, null, null };
        yield return new object[] { string.Empty, new(), string.Empty, true, DataSourceUpdateMode.OnValidation - 1, DBNull.Value, string.Empty };
        yield return new object[] { "propertyName", new(), "dataMember", false, DataSourceUpdateMode.Never, new(), "formatString" };
        yield return new object[] { "propertyName", new(), "dataMember.subDataMember", false, DataSourceUpdateMode.Never + 1, new(), "formatString" };
    }
 
    [Theory]
    [MemberData(nameof(Ctor_String_Object_String_Bool_DataSourceUpdateMode_Object_String_TestData))]
    public void Binding_Ctor_String_Object_String_Bool_DataSourceUpdateMode_Object_String(string propertyName, object dataSource, string dataMember, bool formattingEnabled, DataSourceUpdateMode dataSourceUpdateMode, object nullValue, string formatString)
    {
        Binding binding = new(propertyName, dataSource, dataMember, formattingEnabled, dataSourceUpdateMode, nullValue, formatString);
        Assert.Null(binding.BindableComponent);
        Assert.Null(binding.BindingManagerBase);
        Assert.Equal(new BindingMemberInfo(dataMember), binding.BindingMemberInfo);
        Assert.Null(binding.Control);
        Assert.Equal(ControlUpdateMode.OnPropertyChanged, binding.ControlUpdateMode);
        Assert.Same(dataSource, binding.DataSource);
        Assert.Same(DBNull.Value, binding.DataSourceNullValue);
        Assert.Equal(dataSourceUpdateMode, binding.DataSourceUpdateMode);
        Assert.Null(binding.FormatInfo);
        Assert.Same(formatString, binding.FormatString);
        Assert.Equal(formattingEnabled, binding.FormattingEnabled);
        Assert.False(binding.IsBinding);
        Assert.Same(nullValue, binding.NullValue);
        Assert.Same(propertyName, binding.PropertyName);
    }
 
    public static IEnumerable<object[]> Ctor_String_Object_String_Bool_DataSourceUpdateMode_Object_String_IFormatProvider_TestData()
    {
        yield return new object[] { null, null, null, true, DataSourceUpdateMode.OnValidation, null, null, null };
        yield return new object[] { string.Empty, new(), string.Empty, true, DataSourceUpdateMode.OnValidation - 1, DBNull.Value, string.Empty, CultureInfo.CurrentCulture };
        yield return new object[] { "propertyName", new(), "dataMember", false, DataSourceUpdateMode.Never, new(), "formatString", CultureInfo.InvariantCulture };
        yield return new object[] { "propertyName", new(), "dataMember.subDataMember", false, DataSourceUpdateMode.Never + 1, new(), "formatString", CultureInfo.CurrentCulture };
    }
 
    [Theory]
    [MemberData(nameof(Ctor_String_Object_String_Bool_DataSourceUpdateMode_Object_String_IFormatProvider_TestData))]
    public void Binding_Ctor_String_Object_String_Bool_DataSourceUpdateMode_Object_String_IFormatProvider(string propertyName, object dataSource, string dataMember, bool formattingEnabled, DataSourceUpdateMode dataSourceUpdateMode, object nullValue, string formatString, IFormatProvider formatInfo)
    {
        Binding binding = new(propertyName, dataSource, dataMember, formattingEnabled, dataSourceUpdateMode, nullValue, formatString, formatInfo);
        Assert.Null(binding.BindableComponent);
        Assert.Null(binding.BindingManagerBase);
        Assert.Equal(new BindingMemberInfo(dataMember), binding.BindingMemberInfo);
        Assert.Null(binding.Control);
        Assert.Equal(ControlUpdateMode.OnPropertyChanged, binding.ControlUpdateMode);
        Assert.Same(dataSource, binding.DataSource);
        Assert.Same(DBNull.Value, binding.DataSourceNullValue);
        Assert.Equal(dataSourceUpdateMode, binding.DataSourceUpdateMode);
        Assert.Equal(formatInfo, binding.FormatInfo);
        Assert.Same(formatString, binding.FormatString);
        Assert.Equal(formattingEnabled, binding.FormattingEnabled);
        Assert.False(binding.IsBinding);
        Assert.Same(nullValue, binding.NullValue);
        Assert.Same(propertyName, binding.PropertyName);
    }
 
    public static IEnumerable<object[]> DataSourceNullValue_Set_TestData()
    {
        yield return new object[] { null };
        yield return new object[] { new() };
        yield return new object[] { DBNull.Value };
    }
 
    [Theory]
    [MemberData(nameof(DataSourceNullValue_Set_TestData))]
    public void Binding_DataSourceNullValue_Set_GetReturnsExpected(object value)
    {
        Binding binding = new("propertyName", new object(), "dataMember")
        {
            DataSourceNullValue = value
        };
        Assert.Equal(value, binding.DataSourceNullValue);
 
        // Set same.
        binding.DataSourceNullValue = value;
        Assert.Equal(value, binding.DataSourceNullValue);
    }
 
    [Theory]
    [EnumData<ControlUpdateMode>]
    [InvalidEnumData<ControlUpdateMode>]
    public void Binding_ControlUpdateMode_Set_GetReturnsExpected(ControlUpdateMode value)
    {
        Binding binding = new("propertyName", new object(), "dataMember")
        {
            ControlUpdateMode = value
        };
        Assert.Equal(value, binding.ControlUpdateMode);
 
        // Set same.
        binding.ControlUpdateMode = value;
        Assert.Equal(value, binding.ControlUpdateMode);
    }
 
    [Theory]
    [EnumData<DataSourceUpdateMode>]
    [InvalidEnumData<DataSourceUpdateMode>]
    public void Binding_DataSourceUpdateMode_Set_GetReturnsExpected(DataSourceUpdateMode value)
    {
        Binding binding = new("propertyName", new object(), "dataMember")
        {
            DataSourceUpdateMode = value
        };
        Assert.Equal(value, binding.DataSourceUpdateMode);
 
        // Set same.
        binding.DataSourceUpdateMode = value;
        Assert.Equal(value, binding.DataSourceUpdateMode);
    }
 
    public static IEnumerable<object[]> FormatInfo_Set_TestData()
    {
        yield return new object[] { null };
        yield return new object[] { CultureInfo.CurrentCulture };
    }
 
    [Theory]
    [MemberData(nameof(FormatInfo_Set_TestData))]
    public void Binding_FormatInfo_Set_GetReturnsExpected(IFormatProvider value)
    {
        Binding binding = new("propertyName", new object(), "dataMember")
        {
            FormatInfo = value
        };
        Assert.Same(value, binding.FormatInfo);
 
        // Set same.
        binding.FormatInfo = value;
        Assert.Same(value, binding.FormatInfo);
    }
 
    [Theory]
    [NormalizedStringData]
    public void Binding_FormatString_Set_GetReturnsExpected(string value, string expected)
    {
        Binding binding = new("propertyName", new object(), "dataMember")
        {
            FormatString = value
        };
        Assert.Equal(expected, binding.FormatString);
 
        // Set same.
        binding.FormatString = value;
        Assert.Equal(expected, binding.FormatString);
    }
 
    [Theory]
    [BoolData]
    public void Binding_FormattingEnabled_Set_GetReturnsExpected(bool value)
    {
        Binding binding = new("propertyName", new object(), "dataMember")
        {
            FormattingEnabled = value
        };
        Assert.Equal(value, binding.FormattingEnabled);
 
        // Set same.
        binding.FormattingEnabled = value;
        Assert.Equal(value, binding.FormattingEnabled);
 
        // Set different.
        binding.FormattingEnabled = !value;
        Assert.Equal(!value, binding.FormattingEnabled);
    }
 
    public static IEnumerable<object[]> NullValue_Set_TestData()
    {
        yield return new object[] { null };
        yield return new object[] { new() };
        yield return new object[] { DBNull.Value };
    }
 
    [Theory]
    [MemberData(nameof(NullValue_Set_TestData))]
    public void Binding_NullValue_Set_GetReturnsExpected(object value)
    {
        Binding binding = new("propertyName", new object(), "dataMember")
        {
            NullValue = value
        };
        Assert.Same(value, binding.NullValue);
 
        // Set same.
        binding.NullValue = value;
        Assert.Same(value, binding.NullValue);
    }
 
    public static IEnumerable<object[]> BindingCompleteEventArgs_TestData()
    {
        yield return new object[] { null };
        yield return new object[] { new BindingCompleteEventArgs(null, BindingCompleteState.Success, BindingCompleteContext.ControlUpdate) };
    }
 
    [Theory]
    [MemberData(nameof(BindingCompleteEventArgs_TestData))]
    public void Binding_OnBindingComplete_Invoke_CallsBindingComplete(BindingCompleteEventArgs eventArgs)
    {
        SubBinding binding = new("propertyName", new object(), "dataMember");
 
        // No handler.
        binding.OnBindingComplete(eventArgs);
 
        // Handler.
        int callCount = 0;
        BindingCompleteEventHandler handler = (sender, e) =>
        {
            Assert.Equal(binding, sender);
            Assert.Same(eventArgs, e);
            callCount++;
        };
 
        binding.BindingComplete += handler;
        binding.OnBindingComplete(eventArgs);
        Assert.Equal(1, callCount);
 
        // Should not call if the handler is removed.
        binding.BindingComplete -= handler;
        binding.OnBindingComplete(eventArgs);
        Assert.Equal(1, callCount);
    }
 
    [Theory]
    [MemberData(nameof(BindingCompleteEventArgs_TestData))]
    public void Binding_OnBindingComplete_InvokeInsideBindingComplete_DoesNotCallBindingComplete(BindingCompleteEventArgs eventArgs)
    {
        SubBinding binding = new("propertyName", new object(), "dataMember");
 
        int callCount = 0;
        BindingCompleteEventHandler handler = (sender, e) =>
        {
            Assert.Equal(binding, sender);
            Assert.Same(eventArgs, e);
            callCount++;
 
            binding.OnBindingComplete(e);
        };
 
        binding.BindingComplete += handler;
        binding.OnBindingComplete(eventArgs);
        Assert.Equal(1, callCount);
    }
 
    public static IEnumerable<object[]> OnBindingComplete_CriticalException_TestData()
    {
#pragma warning disable CA2201 // Do not raise reserved exception types
        yield return new object[] { null, new NullReferenceException() };
        yield return new object[] { new BindingCompleteEventArgs(null, BindingCompleteState.Success, BindingCompleteContext.ControlUpdate), new NullReferenceException() };
#pragma warning restore CA2201
    }
 
    [Theory]
    [MemberData(nameof(OnBindingComplete_CriticalException_TestData))]
    public void Binding_OnBindingComplete_ThrowsCriticalException_Rethrows(BindingCompleteEventArgs eventArgs, Exception exception)
    {
        SubBinding binding = new("propertyName", new object(), "dataMember");
 
        int callCount = 0;
        BindingCompleteEventHandler handler = (sender, e) =>
        {
            Assert.Equal(binding, sender);
            Assert.Same(eventArgs, e);
            callCount++;
 
            throw exception;
        };
 
        binding.BindingComplete += handler;
        Assert.Throws(exception.GetType(), () => binding.OnBindingComplete(eventArgs));
        if (eventArgs is not null)
        {
            Assert.False(eventArgs.Cancel);
        }
 
        Assert.Equal(1, callCount);
    }
 
    public static IEnumerable<object[]> OnBindingComplete_NonCriticalException_TestData()
    {
        yield return new object[] { null, new SecurityException() };
        yield return new object[] { new BindingCompleteEventArgs(null, BindingCompleteState.Success, BindingCompleteContext.ControlUpdate), new SecurityException() };
    }
 
    [Theory]
    [MemberData(nameof(OnBindingComplete_NonCriticalException_TestData))]
    public void Binding_OnBindingComplete_ThrowsNonCriticalException_SetsCancelToTrue(BindingCompleteEventArgs eventArgs, Exception exception)
    {
        SubBinding binding = new("propertyName", new object(), "dataMember");
 
        int callCount = 0;
        BindingCompleteEventHandler handler = (sender, e) =>
        {
            Assert.Equal(binding, sender);
            Assert.Same(eventArgs, e);
            callCount++;
 
            throw exception;
        };
 
        binding.BindingComplete += handler;
        binding.OnBindingComplete(eventArgs);
        if (eventArgs is not null)
        {
            Assert.True(eventArgs.Cancel);
        }
 
        Assert.Equal(1, callCount);
    }
 
    public static IEnumerable<object[]> ConvertEventArgs_TestData()
    {
        yield return new object[] { false, null, null };
        yield return new object[] { false, new ConvertEventArgs(null, typeof(int)), null };
        yield return new object[] { false, new ConvertEventArgs("1", null), "1" };
        yield return new object[] { false, new ConvertEventArgs("1", typeof(string)), "1" };
        yield return new object[] { false, new ConvertEventArgs("1", typeof(int)), 1 };
        yield return new object[] { false, new ConvertEventArgs(1.1.ToString(CultureInfo.CurrentCulture), typeof(double)), 1.1 };
        yield return new object[] { false, new ConvertEventArgs(DBNull.Value, typeof(int)), DBNull.Value };
 
        object o = new();
        yield return new object[] { false, new ConvertEventArgs(o, typeof(object)), o };
        yield return new object[] { false, new ConvertEventArgs(o, typeof(int)), o };
 
        yield return new object[] { true, null, null };
        yield return new object[] { true, new ConvertEventArgs("1", typeof(int)), "1" };
    }
 
    [Theory]
    [MemberData(nameof(ConvertEventArgs_TestData))]
    public void Binding_OnFormat_Invoke_CallsFormat(bool formattingEnabled, ConvertEventArgs eventArgs, object expectedValue)
    {
        SubBinding binding = new("propertyName", new object(), "dataMember")
        {
            FormattingEnabled = formattingEnabled
        };
 
        // No handler.
        object oldValue = eventArgs?.Value;
        binding.OnFormat(eventArgs);
        Assert.Equal(expectedValue, eventArgs?.Value);
 
        // Handler.
        int callCount = 0;
        ConvertEventHandler handler = (sender, e) =>
        {
            Assert.Equal(binding, sender);
            Assert.Same(eventArgs, e);
            callCount++;
        };
 
        binding.Format += handler;
        if (eventArgs is not null)
        {
            eventArgs.Value = oldValue;
        }
 
        binding.OnFormat(eventArgs);
        Assert.Equal(expectedValue, eventArgs?.Value);
        Assert.Equal(1, callCount);
 
        // Should not call if the handler is removed.
        binding.Format -= handler;
        if (eventArgs is not null)
        {
            eventArgs.Value = oldValue;
        }
 
        binding.OnFormat(eventArgs);
        Assert.Equal(expectedValue, eventArgs?.Value);
        Assert.Equal(1, callCount);
    }
 
    [Theory]
    [MemberData(nameof(ConvertEventArgs_TestData))]
    public void Binding_OnParse_Invoke_CallsParse(bool formattingEnabled, ConvertEventArgs eventArgs, object expectedValue)
    {
        SubBinding binding = new("propertyName", new object(), "dataMember")
        {
            FormattingEnabled = formattingEnabled
        };
 
        // No handler.
        object oldValue = eventArgs?.Value;
        binding.OnParse(eventArgs);
        Assert.Equal(expectedValue, eventArgs?.Value);
 
        // Handler.
        int callCount = 0;
        ConvertEventHandler handler = (sender, e) =>
        {
            Assert.Equal(binding, sender);
            Assert.Same(eventArgs, e);
            callCount++;
        };
 
        binding.Parse += handler;
        if (eventArgs is not null)
        {
            eventArgs.Value = oldValue;
        }
 
        binding.OnParse(eventArgs);
        Assert.Equal(expectedValue, eventArgs?.Value);
        Assert.Equal(1, callCount);
 
        // Should not call if the handler is removed.
        binding.Parse -= handler;
        if (eventArgs is not null)
        {
            eventArgs.Value = oldValue;
        }
 
        binding.OnParse(eventArgs);
        Assert.Equal(expectedValue, eventArgs?.Value);
        Assert.Equal(1, callCount);
    }
 
    public static IEnumerable<object[]> ReadValue_TestData()
    {
        foreach (ControlUpdateMode controlUpdateMode in Enum.GetValues(typeof(ControlUpdateMode)))
        {
            yield return new object[] { controlUpdateMode, true, 1 };
            yield return new object[] { controlUpdateMode, false, 0 };
        }
    }
 
    [Theory]
    [MemberData(nameof(ReadValue_TestData))]
    public void Binding_ReadValue_Invoke_CallsBindingCompleteIfFormattingEnabled(ControlUpdateMode controlUpdateMode, bool formattingEnabled, int expectedCallCount)
    {
        Binding binding = new("propertyName", new object(), "dataMember")
        {
            ControlUpdateMode = controlUpdateMode,
            FormattingEnabled = formattingEnabled
        };
 
        // No handler.
        binding.ReadValue();
 
        // Handler.
        int callCount = 0;
        BindingCompleteEventHandler handler = (sender, e) =>
        {
            Assert.Same(binding, sender);
            Assert.Same(binding, e.Binding);
            Assert.Equal(BindingCompleteContext.ControlUpdate, e.BindingCompleteContext);
            Assert.Equal(BindingCompleteState.Success, e.BindingCompleteState);
            Assert.False(e.Cancel);
            Assert.Empty(e.ErrorText);
            Assert.Null(e.Exception);
            callCount++;
        };
 
        binding.BindingComplete += handler;
        binding.ReadValue();
        Assert.Equal(expectedCallCount, callCount);
 
        // Should not call if the handler is removed.
        binding.BindingComplete -= handler;
        binding.ReadValue();
        Assert.Equal(expectedCallCount, callCount);
    }
 
    public static IEnumerable<object[]> WriteValue_TestData()
    {
        foreach (ControlUpdateMode controlUpdateMode in Enum.GetValues(typeof(ControlUpdateMode)))
        {
            yield return new object[] { controlUpdateMode, true };
            yield return new object[] { controlUpdateMode, false };
        }
    }
 
    [Theory]
    [MemberData(nameof(WriteValue_TestData))]
    public void Binding_WriteValue_Invoke_DoesNotCallBindingComplete(ControlUpdateMode controlUpdateMode, bool formattingEnabled)
    {
        Binding binding = new("propertyName", new object(), "dataMember")
        {
            ControlUpdateMode = controlUpdateMode,
            FormattingEnabled = formattingEnabled
        };
 
        // No handler.
        binding.WriteValue();
 
        // Handler.
        int callCount = 0;
        BindingCompleteEventHandler handler = (sender, e) => callCount++;
 
        binding.BindingComplete += handler;
        binding.WriteValue();
        Assert.Equal(0, callCount);
 
        // Should not call if the handler is removed.
        binding.BindingComplete -= handler;
        binding.WriteValue();
        Assert.Equal(0, callCount);
    }
 
    private class SubBinding : Binding
    {
        public SubBinding(string propertyName, object dataSource, string dataMember) : base(propertyName, dataSource, dataMember)
        {
        }
 
        public new void OnBindingComplete(BindingCompleteEventArgs e) => base.OnBindingComplete(e);
 
        public new void OnFormat(ConvertEventArgs cevent) => base.OnFormat(cevent);
 
        public new void OnParse(ConvertEventArgs cevent) => base.OnParse(cevent);
    }
}