// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CommunityToolkit.Mvvm.UnitTests; [TestClass] public partial class Test_ObservablePropertyAttribute { [TestMethod] public void Test_ObservablePropertyAttribute_Events() { SampleModel model = new(); (PropertyChangingEventArgs, int) changing = default; (PropertyChangedEventArgs, int) changed = default; model.PropertyChanging += (s, e) => { Assert.IsNull(changing.Item1); Assert.IsNull(changed.Item1); Assert.AreSame(model, s); Assert.IsNotNull(s); Assert.IsNotNull(e); changing = (e, model.Data); }; model.PropertyChanged += (s, e) => { Assert.IsNotNull(changing.Item1); Assert.IsNull(changed.Item1); Assert.AreSame(model, s); Assert.IsNotNull(s); Assert.IsNotNull(e); changed = (e, model.Data); }; model.Data = 42; Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Data)); Assert.AreEqual(changing.Item2, 0); Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Data)); Assert.AreEqual(changed.Item2, 42); } // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4225 [TestMethod] public void Test_ObservablePropertyAttributeWithinRegion_Events() { SampleModel model = new(); (PropertyChangingEventArgs, int) changing = default; (PropertyChangedEventArgs, int) changed = default; model.PropertyChanging += (s, e) => { Assert.IsNull(changing.Item1); Assert.IsNull(changed.Item1); Assert.AreSame(model, s); Assert.IsNotNull(s); Assert.IsNotNull(e); changing = (e, model.Counter); }; model.PropertyChanged += (s, e) => { Assert.IsNotNull(changing.Item1); Assert.IsNull(changed.Item1); Assert.AreSame(model, s); Assert.IsNotNull(s); Assert.IsNotNull(e); changed = (e, model.Counter); }; model.Counter = 42; Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Counter)); Assert.AreEqual(changing.Item2, 0); Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Counter)); Assert.AreEqual(changed.Item2, 42); } // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4225 [TestMethod] public void Test_ObservablePropertyAttributeRightBelowRegion_Events() { SampleModel model = new(); (PropertyChangingEventArgs, string?) changing = default; (PropertyChangedEventArgs, string?) changed = default; model.PropertyChanging += (s, e) => { Assert.IsNull(changing.Item1); Assert.IsNull(changed.Item1); Assert.AreSame(model, s); Assert.IsNotNull(s); Assert.IsNotNull(e); changing = (e, model.Name); }; model.PropertyChanged += (s, e) => { Assert.IsNotNull(changing.Item1); Assert.IsNull(changed.Item1); Assert.AreSame(model, s); Assert.IsNotNull(s); Assert.IsNotNull(e); changed = (e, model.Name); }; model.Name = "Bob"; Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Name)); Assert.AreEqual(changing.Item2, null); Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Name)); Assert.AreEqual(changed.Item2, "Bob"); } [TestMethod] public void Test_AlsoNotifyChangeForAttribute_Events() { DependentPropertyModel model = new(); List propertyNames = new(); model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); model.Name = "Bob"; model.Surname = "Ross"; CollectionAssert.AreEqual(new[] { nameof(model.Name), nameof(model.FullName), nameof(model.Alias), nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames); } [TestMethod] public void Test_ValidationAttributes() { PropertyInfo nameProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Name))!; Assert.IsNotNull(nameProperty.GetCustomAttribute()); Assert.IsNotNull(nameProperty.GetCustomAttribute()); Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 1); Assert.IsNotNull(nameProperty.GetCustomAttribute()); Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 100); PropertyInfo ageProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Age))!; Assert.IsNotNull(ageProperty.GetCustomAttribute()); Assert.AreEqual(ageProperty.GetCustomAttribute()!.Minimum, 0); Assert.AreEqual(ageProperty.GetCustomAttribute()!.Maximum, 120); PropertyInfo emailProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Email))!; Assert.IsNotNull(emailProperty.GetCustomAttribute()); PropertyInfo comboProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.IfThisWorksThenThatsGreat))!; TestValidationAttribute testAttribute = comboProperty.GetCustomAttribute()!; Assert.IsNotNull(testAttribute); Assert.IsNull(testAttribute.O); Assert.AreEqual(testAttribute.T, typeof(SampleModel)); Assert.AreEqual(testAttribute.Flag, true); Assert.AreEqual(testAttribute.D, 6.28); CollectionAssert.AreEqual(testAttribute.Names, new[] { "Bob", "Ross" }); object[]? nestedArray = (object[]?)testAttribute.NestedArray; Assert.IsNotNull(nestedArray); Assert.AreEqual(nestedArray!.Length, 3); Assert.AreEqual(nestedArray[0], 1); Assert.AreEqual(nestedArray[1], "Hello"); Assert.IsTrue(nestedArray[2] is int[]); CollectionAssert.AreEqual((int[])nestedArray[2], new[] { 2, 3, 4 }); Assert.AreEqual(testAttribute.Animal, Animal.Llama); } // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4216 [TestMethod] public void Test_ObservablePropertyWithValueNamedField() { ModelWithValueProperty model = new(); List propertyNames = new(); model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); model.Value = "Hello world"; Assert.AreEqual(model.Value, "Hello world"); CollectionAssert.AreEqual(new[] { nameof(model.Value) }, propertyNames); } // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4216 [TestMethod] public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributes() { ModelWithValuePropertyWithValidation model = new(); List propertyNames = new(); model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); model.Value = "Hello world"; Assert.AreEqual(model.Value, "Hello world"); CollectionAssert.AreEqual(new[] { nameof(model.Value) }, propertyNames); } // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4184 [TestMethod] public void Test_GeneratedPropertiesWithValidationAttributesOverFields() { ViewModelWithValidatableGeneratedProperties model = new(); List propertyNames = new(); model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); // Assign these fields directly to bypass the validation that is executed in the generated setters. // We only need those generated properties to be there to check whether they are correctly detected. model.first = "A"; model.last = "This is a very long name that exceeds the maximum length of 60 for this property"; Assert.IsFalse(model.HasErrors); model.RunValidation(); Assert.IsTrue(model.HasErrors); ValidationResult[] validationErrors = model.GetErrors().ToArray(); Assert.AreEqual(validationErrors.Length, 2); CollectionAssert.AreEqual(new[] { nameof(ViewModelWithValidatableGeneratedProperties.First) }, validationErrors[0].MemberNames.ToArray()); CollectionAssert.AreEqual(new[] { nameof(ViewModelWithValidatableGeneratedProperties.Last) }, validationErrors[1].MemberNames.ToArray()); } [TestMethod] public void Test_AlsoNotifyChangeFor() { DependentPropertyModel model = new(); List propertyNames = new(); int canExecuteRequests = 0; model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; model.Surname = "Ross"; Assert.AreEqual(1, canExecuteRequests); CollectionAssert.AreEqual(new[] { nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames); } public partial class SampleModel : ObservableObject { /// /// This is a sample data field within of type . /// [ObservableProperty] private int data; #region More properties [ObservableProperty] private int counter; #endregion [ObservableProperty] private string? name; } [INotifyPropertyChanged] public sealed partial class DependentPropertyModel { [ObservableProperty] [AlsoNotifyChangeFor(nameof(FullName))] [AlsoNotifyChangeFor(nameof(Alias))] private string? name; [ObservableProperty] [AlsoNotifyChangeFor(nameof(FullName), nameof(Alias))] [AlsoNotifyCanExecuteFor(nameof(MyCommand))] private string? surname; public string FullName => $"{Name} {Surname}"; public string Alias => $"{Name?.ToLower()}{Surname?.ToLower()}"; public RelayCommand MyCommand { get; } = new(() => { }); } public partial class MyFormViewModel : ObservableValidator { [ObservableProperty] [Required] [MinLength(1)] [MaxLength(100)] private string? name; [ObservableProperty] [Range(0, 120)] private int age; [ObservableProperty] [EmailAddress] private string? email; [ObservableProperty] [TestValidation(null, typeof(SampleModel), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Animal.Llama)] private int ifThisWorksThenThatsGreat; } private sealed class TestValidationAttribute : ValidationAttribute { public TestValidationAttribute(object? o, Type t, bool flag, double d, string[] names) { O = o; T = t; Flag = flag; D = d; Names = names; } public object? O { get; } public Type T { get; } public bool Flag { get; } public double D { get; } public string[] Names { get; } public object? NestedArray { get; set; } public Animal Animal { get; set; } } public enum Animal { Cat, Dog, Llama } public partial class ModelWithValueProperty : ObservableObject { [ObservableProperty] private string? value; } public partial class ModelWithValuePropertyWithValidation : ObservableValidator { [ObservableProperty] [Required] [MinLength(5)] private string? value; } public partial class ViewModelWithValidatableGeneratedProperties : ObservableValidator { [Required] [MinLength(2)] [MaxLength(60)] [Display(Name = "FirstName")] [ObservableProperty] public string first = "Bob"; [Display(Name = "LastName")] [Required] [MinLength(2)] [MaxLength(60)] [ObservableProperty] public string last = "Jones"; public void RunValidation() => ValidateAllProperties(); } }