Browse Source

Merge pull request #5998 from AvaloniaUI/nullable/diagnostics

Enable nullable on Avalonia.Diagnostics project
Max Katz 4 years ago
parent
commit
74dbc5b26c
36 changed files with 319 additions and 227 deletions
  1. 79 2
      src/Avalonia.Base/Metadata/NullableAttributes.cs
  2. 4 0
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  3. 4 4
      src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
  4. 8 3
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
  5. 2 2
      src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
  6. 4 6
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  7. 2 2
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
  8. 1 3
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  9. 2 2
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  10. 16 29
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  11. 8 5
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  12. 2 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
  13. 19 16
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  14. 6 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
  15. 1 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  16. 11 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  17. 3 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
  18. 5 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
  19. 10 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs
  20. 6 9
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  21. 5 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  22. 14 9
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  23. 4 4
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  24. 5 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
  25. 6 4
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
  26. 8 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  27. 13 26
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
  28. 7 7
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  29. 6 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
  30. 5 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  31. 9 5
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
  32. 1 1
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
  33. 6 2
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
  34. 22 11
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  35. 14 10
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  36. 1 1
      src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs

+ 79 - 2
src/Avalonia.Base/Metadata/NullableAttributes.cs

@@ -1,6 +1,5 @@
 #pragma warning disable MA0048 // File name must match type name
 #define INTERNAL_NULLABLE_ATTRIBUTES
-#if NETSTANDARD2_0 ||  NETCOREAPP2_0 ||  NETCOREAPP2_1 ||  NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
 
 // https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs
 
@@ -10,6 +9,7 @@
 
 namespace System.Diagnostics.CodeAnalysis
 {
+#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
     /// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
     [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
 #if INTERNAL_NULLABLE_ATTRIBUTES
@@ -136,5 +136,82 @@ namespace System.Diagnostics.CodeAnalysis
         /// <summary>Gets the condition parameter value.</summary>
         public bool ParameterValue { get; }
     }
-}
+#endif // NETSTANDARD2_0 attributes
+
+#if NETSTANDARD2_1 || NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
+    /// <summary>
+    /// Specifies that the method or property will ensure that the listed field and property members have
+    /// not-<see langword="null"/> values.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
+#if INTERNAL_NULLABLE_ATTRIBUTES
+    internal
+#else
+    public
 #endif
+    sealed class MemberNotNullAttribute : Attribute
+    {
+        /// <summary>Gets field or property member names.</summary>
+        public string[] Members { get; }
+
+        /// <summary>Initializes the attribute with a field or property member.</summary>
+        /// <param name="member">The field or property member that is promised to be not-null.</param>
+        public MemberNotNullAttribute(string member)
+        {
+            Members = new[] { member };
+        }
+
+        /// <summary>Initializes the attribute with the list of field and property members.</summary>
+        /// <param name="members">The list of field and property members that are promised to be not-null.</param>
+        public MemberNotNullAttribute(params string[] members)
+        {
+            Members = members;
+        }
+    }
+
+    /// <summary>
+    /// Specifies that the method or property will ensure that the listed field and property members have
+    /// non-<see langword="null"/> values when returning with the specified return value condition.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
+#if INTERNAL_NULLABLE_ATTRIBUTES
+    internal
+#else
+    public
+#endif
+    sealed class MemberNotNullWhenAttribute : Attribute
+    {
+        /// <summary>Gets the return value condition.</summary>
+        public bool ReturnValue { get; }
+
+        /// <summary>Gets field or property member names.</summary>
+        public string[] Members { get; }
+
+        /// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
+        /// <param name="returnValue">
+        /// The return value condition. If the method returns this value,
+        /// the associated parameter will not be <see langword="null"/>.
+        /// </param>
+        /// <param name="member">The field or property member that is promised to be not-<see langword="null"/>.</param>
+        public MemberNotNullWhenAttribute(bool returnValue, string member)
+        {
+            ReturnValue = returnValue;
+            Members = new[] { member };
+        }
+
+        /// <summary>Initializes the attribute with the specified return value condition and list of field and property members.
+        /// </summary>
+        /// <param name="returnValue">
+        /// The return value condition. If the method returns this value,
+        /// the associated parameter will not be <see langword="null"/>.
+        /// </param>
+        /// <param name="members">The list of field and property members that are promised to be not-null.</param>
+        public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
+        {
+            ReturnValue = returnValue;
+            Members = members;
+        }
+    }
+#endif // NETSTANDARD2_1 attributes
+}
+

+ 4 - 0
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@@ -3,12 +3,16 @@
     <TargetFramework>netstandard2.0</TargetFramework>
     <RootNamespace>Avalonia</RootNamespace>
     <PackageId>Avalonia.Diagnostics</PackageId>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
   <ItemGroup>
     <Compile Update="**\*.xaml.cs">
       <DependentUpon>%(Filename)</DependentUpon>
     </Compile>
   </ItemGroup>
+  <ItemGroup>
+    <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
+  </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
     <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />

+ 4 - 4
src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs

@@ -10,8 +10,8 @@ namespace Avalonia.Diagnostics.Controls
             AvaloniaProperty.RegisterDirect<ThicknessEditor, Thickness>(nameof(Thickness), o => o.Thickness,
                 (o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay);
 
-        public static readonly DirectProperty<ThicknessEditor, string> HeaderProperty =
-            AvaloniaProperty.RegisterDirect<ThicknessEditor, string>(nameof(Header), o => o.Header,
+        public static readonly DirectProperty<ThicknessEditor, string?> HeaderProperty =
+            AvaloniaProperty.RegisterDirect<ThicknessEditor, string?>(nameof(Header), o => o.Header,
                 (o, v) => o.Header = v);
 
         public static readonly DirectProperty<ThicknessEditor, bool> IsPresentProperty =
@@ -36,7 +36,7 @@ namespace Avalonia.Diagnostics.Controls
             AvaloniaProperty.Register<ThicknessEditor, IBrush>(nameof(Highlight));
 
         private Thickness _thickness;
-        private string _header;
+        private string? _header;
         private bool _isPresent = true;
         private double _left;
         private double _top;
@@ -50,7 +50,7 @@ namespace Avalonia.Diagnostics.Controls
             set => SetAndRaise(ThicknessProperty, ref _thickness, value);
         }
 
-        public string Header
+        public string? Header
         {
             get => _header;
             set => SetAndRaise(HeaderProperty, ref _header, value);

+ 8 - 3
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs

@@ -8,12 +8,17 @@ namespace Avalonia.Diagnostics.Converters
     {
         public double Opacity { get; set; }
 
-        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
         {
-            return (bool)value ? 1d : Opacity;
+            if (value is bool boolean && boolean)
+            {
+                return 1d;
+            }
+
+            return Opacity;
         }
 
-        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
         {
             throw new NotImplementedException();
         }

+ 2 - 2
src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs

@@ -7,12 +7,12 @@ namespace Avalonia.Diagnostics.Converters
 {
     internal class EnumToCheckedConverter : IValueConverter
     {
-        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
         {
             return Equals(value, parameter);
         }
 
-        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
         {
             if (value is bool isChecked && isChecked)
             {

+ 4 - 6
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@@ -6,8 +6,6 @@ using Avalonia.Diagnostics.Views;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 
-#nullable enable 
-
 namespace Avalonia.Diagnostics
 {
     public static class DevTools
@@ -24,7 +22,7 @@ namespace Avalonia.Diagnostics
 
         public static IDisposable Attach(TopLevel root, DevToolsOptions options)
         {
-            void PreviewKeyDown(object sender, KeyEventArgs e)
+            void PreviewKeyDown(object? sender, KeyEventArgs e)
             {
                 if (options.Gesture.Matches(e))
                 {
@@ -71,10 +69,10 @@ namespace Avalonia.Diagnostics
             return Disposable.Create(() => window?.Close());
         }
 
-        private static void DevToolsClosed(object sender, EventArgs e)
+        private static void DevToolsClosed(object? sender, EventArgs e)
         {
-            var window = (MainWindow)sender;
-            s_open.Remove(window.Root);
+            var window = (MainWindow)sender!;
+            s_open.Remove(window.Root!);
             window.Closed -= DevToolsClosed;
         }
     }

+ 2 - 2
src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs

@@ -22,8 +22,8 @@ The following commands are available:
 clear(): Clear the output history
 ";
 
-        public dynamic e { get; internal set; }
-        public dynamic root { get; internal set; }
+        public dynamic? e { get; internal set; }
+        public dynamic? root { get; internal set; }
 
         internal static object NoOutput { get; } = new object();
 

+ 1 - 3
src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs

@@ -7,9 +7,7 @@ namespace Avalonia.Diagnostics.Models
     {
         public EventChainLink(object handler, bool handled, RoutingStrategies route)
         {
-            Contract.Requires<ArgumentNullException>(handler != null);
-
-            Handler = handler;
+            Handler = handler ?? throw new ArgumentNullException(nameof(handler));
             Handled = handled;
             Route = route;
         }

+ 2 - 2
src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs

@@ -9,12 +9,12 @@ namespace Avalonia.Diagnostics
     {
         public IControl Build(object data)
         {
-            var name = data.GetType().FullName.Replace("ViewModel", "View");
+            var name = data.GetType().FullName!.Replace("ViewModel", "View");
             var type = Type.GetType(name);
 
             if (type != null)
             {
-                return (Control)Activator.CreateInstance(type);
+                return (Control)Activator.CreateInstance(type)!;
             }
             else
             {

+ 16 - 29
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@@ -1,17 +1,17 @@
-using System.ComponentModel;
-using Avalonia.Collections;
-
 namespace Avalonia.Diagnostics.ViewModels
 {
     internal class AvaloniaPropertyViewModel : PropertyViewModel
     {
         private readonly AvaloniaObject _target;
         private string _type;
-        private object _value;
+        private object? _value;
         private string _priority;
         private string _group;
 
+#nullable disable
+        // Remove "nullable disable" after MemberNotNull will work on our CI.
         public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property)
+#nullable restore
         {
             _target = o;
             Property = property;
@@ -20,12 +20,6 @@ namespace Avalonia.Diagnostics.ViewModels
                 $"[{property.OwnerType.Name}.{property.Name}]" :
                 property.Name;
 
-            if (property.IsDirect)
-            {
-                _group = "Properties";
-                Priority = "Direct";
-            }
-
             Update();
         }
 
@@ -34,11 +28,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public override string Name { get; }
         public bool IsAttached => Property.IsAttached;
 
-        public string Priority
-        {
-            get => _priority;
-            private set => RaiseAndSetIfChanged(ref _priority, value);
-        }
+        public string Priority => _priority;
 
         public override string Type => _type;
 
@@ -56,40 +46,37 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public override string Group
-        {
-            get => _group;
-        }
+        public override string Group => _group;
 
+        // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
         public override void Update()
         {
             if (Property.IsDirect)
             {
                 RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value));
-                RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+                RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
+                RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority));
+
+                _group = "Properties";
             }
             else
             {
                 var val = _target.GetDiagnostic(Property);
 
                 RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value));
-                RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+                RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
 
                 if (val != null)
                 {
-                    SetGroup(IsAttached ? "Attached Properties" : "Properties");
-                    Priority = val.Priority.ToString();
+                    RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority));
+                    RaiseAndSetIfChanged(ref _group, IsAttached ? "Attached Properties" : "Properties", nameof(Group));
                 }
                 else
                 {
-                    SetGroup(Priority = "Unset");
+                    RaiseAndSetIfChanged(ref _priority, "Unset", nameof(Priority));
+                    RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group));
                 }
             }
         }
-
-        private void SetGroup(string group)
-        {
-            RaiseAndSetIfChanged(ref _group, group, nameof(Group));
-        }
     }
 }

+ 8 - 5
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@@ -1,5 +1,4 @@
-using System.ComponentModel;
-using System.Reflection;
+using System.Reflection;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -7,14 +6,17 @@ namespace Avalonia.Diagnostics.ViewModels
     {
         private readonly object _target;
         private string _type;
-        private object _value;
+        private object? _value;
 
+#nullable disable
+        // Remove "nullable disable" after MemberNotNull will work on our CI.
         public ClrPropertyViewModel(object o, PropertyInfo property)
+#nullable restore
         {
             _target = o;
             Property = property;
 
-            if (!property.DeclaringType.IsInterface)
+            if (property.DeclaringType == null || !property.DeclaringType.IsInterface)
             {
                 Name = property.Name;
             }
@@ -47,11 +49,12 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
+        // [MemberNotNull(nameof(_type))]
         public override void Update()
         {
             var val = Property.GetValue(_target);
             RaiseAndSetIfChanged(ref _value, val, nameof(Value));
-            RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+            RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
         }
     }
 }

+ 2 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs

@@ -15,11 +15,12 @@ namespace Avalonia.Diagnostics.ViewModels
         private int _historyIndex = -1;
         private string _input;
         private bool _isVisible;
-        private ScriptState<object> _state;
+        private ScriptState<object>? _state;
 
         public ConsoleViewModel(Action<ConsoleContext> updateContext)
         {
             _context = new ConsoleContext(this);
+            _input = string.Empty;
             _updateContext = updateContext;
         }
 

+ 19 - 16
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -18,10 +18,10 @@ namespace Avalonia.Diagnostics.ViewModels
     {
         private readonly IVisual _control;
         private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
-        private AvaloniaPropertyViewModel _selectedProperty;
+        private AvaloniaPropertyViewModel? _selectedProperty;
         private bool _snapshotStyles;
         private bool _showInactiveStyles;
-        private string _styleStatus;
+        private string? _styleStatus;
 
         public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
         {
@@ -83,7 +83,8 @@ namespace Avalonia.Diagnostics.ViewModels
                     {
                         foreach (var setter in style.Setters)
                         {
-                            if (setter is Setter regularSetter)
+                            if (setter is Setter regularSetter
+                                && regularSetter.Property != null)
                             {
                                 var setterValue = regularSetter.Value;
 
@@ -115,13 +116,14 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        private (object resourceKey, bool isDynamic)? GetResourceInfo(object value)
+        private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value)
         {
             if (value is StaticResourceExtension staticResource)
             {
                 return (staticResource.ResourceKey, false);
             }
-            else if (value is DynamicResourceExtension dynamicResource)
+            else if (value is DynamicResourceExtension dynamicResource
+                && dynamicResource.ResourceKey != null)
             {
                 return (dynamicResource.ResourceKey, true);
             }
@@ -137,7 +139,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
 
-        public AvaloniaPropertyViewModel SelectedProperty
+        public AvaloniaPropertyViewModel? SelectedProperty
         {
             get => _selectedProperty;
             set => RaiseAndSetIfChanged(ref _selectedProperty, value);
@@ -155,7 +157,7 @@ namespace Avalonia.Diagnostics.ViewModels
             set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
         }
 
-        public string StyleStatus
+        public string? StyleStatus
         {
             get => _styleStatus;
             set => RaiseAndSetIfChanged(ref _styleStatus, value);
@@ -248,7 +250,7 @@ namespace Avalonia.Diagnostics.ViewModels
                 .Select(x => new ClrPropertyViewModel(o, x));
         }
 
-        private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+        private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
         {
             if (_propertyIndex.TryGetValue(e.Property, out var properties))
             {
@@ -261,9 +263,10 @@ namespace Avalonia.Diagnostics.ViewModels
             Layout.ControlPropertyChanged(sender, e);
         }
 
-        private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e)
+        private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e)
         {
-            if (_propertyIndex.TryGetValue(e.PropertyName, out var properties))
+            if (e.PropertyName != null
+                && _propertyIndex.TryGetValue(e.PropertyName, out var properties))
             {
                 foreach (var property in properties)
                 {
@@ -277,7 +280,7 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
+        private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
             if (!SnapshotStyles)
             {
@@ -349,10 +352,10 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             public static PropertyComparer Instance { get; } = new PropertyComparer();
 
-            public int Compare(PropertyViewModel x, PropertyViewModel y)
+            public int Compare(PropertyViewModel? x, PropertyViewModel? y)
             {
-                var groupX = GroupIndex(x.Group);
-                var groupY = GroupIndex(y.Group);
+                var groupX = GroupIndex(x?.Group);
+                var groupY = GroupIndex(y?.Group);
 
                 if (groupX != groupY)
                 {
@@ -360,11 +363,11 @@ namespace Avalonia.Diagnostics.ViewModels
                 }
                 else
                 {
-                    return string.CompareOrdinal(x.Name, y.Name);
+                    return string.CompareOrdinal(x?.Name, y?.Name);
                 }
             }
 
-            private int GroupIndex(string group)
+            private int GroupIndex(string? group)
             {
                 switch (group)
                 {

+ 6 - 6
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs

@@ -12,14 +12,14 @@ namespace Avalonia.Diagnostics.ViewModels
         private readonly IVisual _control;
         private Thickness _borderThickness;
         private double _height;
-        private string _heightConstraint;
+        private string? _heightConstraint;
         private HorizontalAlignment _horizontalAlignment;
         private Thickness _marginThickness;
         private Thickness _paddingThickness;
         private bool _updatingFromControl;
         private VerticalAlignment _verticalAlignment;
         private double _width;
-        private string _widthConstraint;
+        private string? _widthConstraint;
 
         public ControlLayoutViewModel(IVisual control)
         {
@@ -80,13 +80,13 @@ namespace Avalonia.Diagnostics.ViewModels
             private set => RaiseAndSetIfChanged(ref _height, value);
         }
 
-        public string WidthConstraint
+        public string? WidthConstraint
         {
             get => _widthConstraint;
             private set => RaiseAndSetIfChanged(ref _widthConstraint, value);
         }
 
-        public string HeightConstraint
+        public string? HeightConstraint
         {
             get => _heightConstraint;
             private set => RaiseAndSetIfChanged(ref _heightConstraint, value);
@@ -112,7 +112,7 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             if (_control is IAvaloniaObject ao)
             {
-                string CreateConstraintInfo(StyledProperty<double> minProperty, StyledProperty<double> maxProperty)
+                string? CreateConstraintInfo(StyledProperty<double> minProperty, StyledProperty<double> maxProperty)
                 {
                     bool hasMin = ao.IsSet(minProperty);
                     bool hasMax = ao.IsSet(maxProperty);
@@ -179,7 +179,7 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+        public void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
         {
             try
             {

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
                     if (_updateChildren && value != null)
                     {
-                        foreach (var child in Children)
+                        foreach (var child in Children!)
                         {
                             try
                             {

+ 11 - 12
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@@ -11,16 +11,13 @@ namespace Avalonia.Diagnostics.ViewModels
     {
         private readonly EventsPageViewModel _parentViewModel;
         private bool _isRegistered;
-        private FiredEvent _currentEvent;
+        private FiredEvent? _currentEvent;
 
         public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm)
             : base(parent, @event.Name)
         {
-            Contract.Requires<ArgumentNullException>(@event != null);
-            Contract.Requires<ArgumentNullException>(vm != null);
-
-            Event = @event;
-            _parentViewModel = vm;
+            Event = @event ?? throw new ArgumentNullException(nameof(@event));
+            _parentViewModel = vm ?? throw new ArgumentNullException(nameof(vm));
         }
 
         public RoutedEvent Event { get; }
@@ -62,18 +59,18 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        private void HandleEvent(object sender, RoutedEventArgs e)
+        private void HandleEvent(object? sender, RoutedEventArgs e)
         {
             if (!_isRegistered || IsEnabled == false)
                 return;
             if (sender is IVisual v && BelongsToDevTool(v))
                 return;
 
-            var s = sender;
+            var s = sender!;
             var handled = e.Handled;
             var route = e.Route;
 
-            Action handler = delegate
+            void handler()
             {
                 if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e))
                 {
@@ -98,14 +95,16 @@ namespace Avalonia.Diagnostics.ViewModels
 
         private static bool BelongsToDevTool(IVisual v)
         {
-            while (v != null)
+            var current = v;
+
+            while (current != null)
             {
-                if (v is MainView || v is MainWindow)
+                if (current is MainView || current is MainWindow)
                 {
                     return true;
                 }
 
-                v = v.VisualParent;
+                current = current.VisualParent;
             }
 
             return false;

+ 3 - 3
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs

@@ -10,14 +10,14 @@ namespace Avalonia.Diagnostics.ViewModels
         private bool? _isEnabled = false;
         private bool _isVisible;
 
-        protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
+        protected EventTreeNodeBase(EventTreeNodeBase? parent, string text)
         {
             Parent = parent;
             Text = text;
             IsVisible = true;
         }
 
-        public IAvaloniaReadOnlyList<EventTreeNodeBase> Children
+        public IAvaloniaReadOnlyList<EventTreeNodeBase>? Children
         {
             get;
             protected set;
@@ -41,7 +41,7 @@ namespace Avalonia.Diagnostics.ViewModels
             set => RaiseAndSetIfChanged(ref _isVisible, value);
         }
 
-        public EventTreeNodeBase Parent
+        public EventTreeNodeBase? Parent
         {
             get;
         }

+ 5 - 6
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
-using System.ComponentModel;
 using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Diagnostics.Models;
@@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels
         };
 
         private readonly MainViewModel _mainViewModel;
-        private FiredEvent _selectedEvent;
-        private EventTreeNodeBase _selectedNode;
+        private FiredEvent? _selectedEvent;
+        private EventTreeNodeBase? _selectedNode;
 
         public EventsPageViewModel(MainViewModel mainViewModel)
         {
@@ -48,13 +47,13 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>();
 
-        public FiredEvent SelectedEvent
+        public FiredEvent? SelectedEvent
         {
             get => _selectedEvent;
             set => RaiseAndSetIfChanged(ref _selectedEvent, value);
         }
 
-        public EventTreeNodeBase SelectedNode
+        public EventTreeNodeBase? SelectedNode
         {
             get => _selectedNode;
             set => RaiseAndSetIfChanged(ref _selectedNode, value);
@@ -99,7 +98,7 @@ namespace Avalonia.Diagnostics.ViewModels
                 }
             }
 
-            static EventTreeNodeBase FindNode(EventTreeNodeBase node, RoutedEvent eventType)
+            static EventTreeNodeBase? FindNode(EventTreeNodeBase node, RoutedEvent eventType)
             {
                 if (node is EventTreeNode eventNode && eventNode.Event == eventType)
                 {

+ 10 - 12
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs

@@ -11,10 +11,13 @@ namespace Avalonia.Diagnostics.ViewModels
         private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();
         private string _filterString = string.Empty;
         private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
-        private string _processedFilter;
-        private Regex _filterRegex;
+        private Regex? _filterRegex;
 
-        public event EventHandler RefreshFilter;
+        public event EventHandler? RefreshFilter;
+
+        public bool HasErrors => _errors.Count > 0;
+
+        public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
 
         public bool Filter(string input)
         {
@@ -31,13 +34,11 @@ namespace Avalonia.Diagnostics.ViewModels
                 }
             }
 
-            _processedFilter = FilterString.Trim();
-
             try
             {
                 var options = RegexOptions.Compiled;
                 var pattern = UseRegexFilter
-                    ? _processedFilter : Regex.Escape(_processedFilter);
+                    ? FilterString.Trim() : Regex.Escape(FilterString.Trim());
                 if (!UseCaseSensitiveFilter)
                 {
                     options |= RegexOptions.IgnoreCase;
@@ -109,16 +110,13 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public IEnumerable GetErrors(string propertyName)
+        public IEnumerable GetErrors(string? propertyName)
         {
-            if (_errors.TryGetValue(propertyName, out var error))
+            if (propertyName != null
+                && _errors.TryGetValue(propertyName, out var error))
             {
                 yield return error;
             }
         }
-
-        public bool HasErrors => _errors.Count > 0;
-
-        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
     }
 }

+ 6 - 9
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs

@@ -8,15 +8,12 @@ namespace Avalonia.Diagnostics.ViewModels
     internal class FiredEvent : ViewModelBase
     {
         private readonly RoutedEventArgs _eventArgs;
-        private EventChainLink _handledBy;
+        private EventChainLink? _handledBy;
 
         public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator)
         {
-            Contract.Requires<ArgumentNullException>(eventArgs != null);
-            Contract.Requires<ArgumentNullException>(originator != null);
-
-            _eventArgs = eventArgs;
-            Originator = originator;
+            _eventArgs = eventArgs ?? throw new ArgumentNullException(nameof(eventArgs));
+            Originator = originator ?? throw new ArgumentNullException(nameof(originator));
             AddToChain(originator);
         }
 
@@ -25,7 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels
             return e == _eventArgs;
         }
 
-        public RoutedEvent Event => _eventArgs.RoutedEvent;
+        public RoutedEvent Event => _eventArgs.RoutedEvent!;
 
         public bool IsHandled => HandledBy?.Handled == true;
 
@@ -38,7 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels
                 if (IsHandled)
                 {
                     return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine +
-                           $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
+                           $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy!.HandlerName}";
                 }
 
                 return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}";
@@ -47,7 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public EventChainLink Originator { get; }
 
-        public EventChainLink HandledBy
+        public EventChainLink? HandledBy
         {
             get => _handledBy;
             set

+ 5 - 3
src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@@ -7,22 +7,24 @@ namespace Avalonia.Diagnostics.ViewModels
 {
     internal class LogicalTreeNode : TreeNode
     {
-        public LogicalTreeNode(ILogical logical, TreeNode parent)
+        public LogicalTreeNode(ILogical logical, TreeNode? parent)
             : base((Control)logical, parent)
         {
             Children = new LogicalTreeNodeCollection(this, logical);
         }
 
+        public override TreeNodeCollection Children { get; }
+
         public static LogicalTreeNode[] Create(object control)
         {
             var logical = control as ILogical;
-            return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
+            return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty<LogicalTreeNode>();
         }
 
         internal class LogicalTreeNodeCollection : TreeNodeCollection
         {
             private readonly ILogical _control;
-            private IDisposable _subscription;
+            private IDisposable? _subscription;
 
             public LogicalTreeNodeCollection(TreeNode owner, ILogical control)
                 : base(owner)

+ 14 - 9
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@@ -1,10 +1,10 @@
 using System;
 using System.ComponentModel;
+
 using Avalonia.Controls;
 using Avalonia.Diagnostics.Models;
 using Avalonia.Input;
 using Avalonia.Threading;
-using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -17,13 +17,16 @@ namespace Avalonia.Diagnostics.ViewModels
         private readonly IDisposable _pointerOverSubscription;
         private ViewModelBase _content;
         private int _selectedTab;
-        private string _focusedControl;
-        private string _pointerOverElement;
+        private string? _focusedControl;
+        private string? _pointerOverElement;
         private bool _shouldVisualizeMarginPadding = true;
         private bool _shouldVisualizeDirtyRects;
         private bool _showFpsOverlay;
 
+#nullable disable
+        // Remove "nullable disable" after MemberNotNull will work on our CI.
         public MainViewModel(TopLevel root)
+#nullable restore
         {
             _root = root;
             _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@@ -84,6 +87,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public ViewModelBase Content
         {
             get { return _content; }
+            // [MemberNotNull(nameof(_content))]
             private set
             {
                 if (_content is TreePageViewModel oldTree &&
@@ -114,34 +118,35 @@ namespace Avalonia.Diagnostics.ViewModels
         public int SelectedTab
         {
             get { return _selectedTab; }
+            // [MemberNotNull(nameof(_content))]
             set
             {
                 _selectedTab = value;
 
                 switch (value)
                 {
-                    case 0:
-                        Content = _logicalTree;
-                        break;
                     case 1:
                         Content = _visualTree;
                         break;
                     case 2:
                         Content = _events;
                         break;
+                    default:
+                        Content = _logicalTree;
+                        break;
                 }
 
                 RaisePropertyChanged();
             }
         }
 
-        public string FocusedControl
+        public string? FocusedControl
         {
             get { return _focusedControl; }
             private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
         }
 
-        public string PointerOverElement
+        public string? PointerOverElement
         {
             get { return _pointerOverElement; }
             private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
@@ -187,7 +192,7 @@ namespace Avalonia.Diagnostics.ViewModels
             FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
         }
 
-        private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e)
+        private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e)
         {
             if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
             {

+ 4 - 4
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public abstract string Value { get; set; }
         public abstract void Update();
 
-        protected static string ConvertToString(object value)
+        protected static string ConvertToString(object? value)
         {
             if (value is null)
             {
@@ -31,13 +31,13 @@ namespace Avalonia.Diagnostics.ViewModels
             if (!converter.CanConvertTo(typeof(string)) || 
                 converter.GetType() == typeof(CollectionConverter))
             {
-                return value.ToString();
+                return value.ToString() ?? "(null)";
             }
 
             return converter.ConvertToString(value);
         }
 
-        private static object InvokeParse(string s, Type targetType)
+        private static object? InvokeParse(string s, Type targetType)
         {
             var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
 
@@ -56,7 +56,7 @@ namespace Avalonia.Diagnostics.ViewModels
             throw new InvalidCastException("Unable to convert value.");
         }
 
-        protected static object ConvertFromString(string s, Type targetType)
+        protected static object? ConvertFromString(string s, Type targetType)
         {
             var converter = TypeDescriptor.GetConverter(targetType);
 

+ 5 - 3
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public IBrush Tint { get; }
 
-        public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue)
+        public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic) : base(property, resourceValue)
         {
             Key = resourceKey;
             Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
@@ -16,12 +16,14 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public void CopyResourceKey()
         {
-            if (Key is null)
+            var textToCopy = Key?.ToString();
+
+            if (textToCopy is null)
             {
                 return;
             }
 
-            CopyToClipboard(Key.ToString());
+            CopyToClipboard(textToCopy);
         }
     }
 }

+ 6 - 4
src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public string Name { get; }
 
-        public object Value { get; }
+        public object? Value { get; }
 
         public bool IsActive
         {
@@ -25,7 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels
             set => RaiseAndSetIfChanged(ref _isVisible, value);
         }
 
-        public SetterViewModel(AvaloniaProperty property, object value)
+        public SetterViewModel(AvaloniaProperty property, object? value)
         {
             Property = property;
             Name = property.Name;
@@ -36,12 +36,14 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public void CopyValue()
         {
-            if (Value is null)
+            var textToCopy = Value?.ToString();
+
+            if (textToCopy is null)
             {
                 return;
             }
 
-            CopyToClipboard(Value.ToString());
+            CopyToClipboard(textToCopy);
         }
 
         public void CopyPropertyName()

+ 8 - 8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@@ -9,17 +9,18 @@ using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class TreeNode : ViewModelBase, IDisposable
+    internal abstract class TreeNode : ViewModelBase, IDisposable
     {
-        private IDisposable _classesSubscription;
+        private IDisposable? _classesSubscription;
         private string _classes;
         private bool _isExpanded;
 
-        public TreeNode(IVisual visual, TreeNode parent)
+        public TreeNode(IVisual visual, TreeNode? parent)
         {
             Parent = parent;
             Type = visual.GetType().Name;
             Visual = visual;
+            _classes = string.Empty;
 
             if (visual is IControl control)
             {
@@ -51,10 +52,9 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public TreeNodeCollection Children
+        public abstract TreeNodeCollection Children
         {
             get;
-            protected set;
         }
 
         public string Classes
@@ -63,7 +63,7 @@ namespace Avalonia.Diagnostics.ViewModels
             private set { RaiseAndSetIfChanged(ref _classes, value); }
         }
 
-        public string ElementName
+        public string? ElementName
         {
             get;
         }
@@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.ViewModels
             set { RaiseAndSetIfChanged(ref _isExpanded, value); }
         }
 
-        public TreeNode Parent
+        public TreeNode? Parent
         {
             get;
         }
@@ -92,7 +92,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public void Dispose()
         {
-            _classesSubscription.Dispose();
+            _classesSubscription?.Dispose();
             Children.Dispose();
         }
 

+ 13 - 26
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs

@@ -3,46 +3,33 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.ComponentModel;
+
 using Avalonia.Collections;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
     internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable
     {
-        private AvaloniaList<TreeNode> _inner;
+        private AvaloniaList<TreeNode>? _inner;
 
         public TreeNodeCollection(TreeNode owner) => Owner = owner;
 
-        public TreeNode this[int index]
-        {
-            get
-            {
-                EnsureInitialized();
-                return _inner[index];
-            }
-        }
+        public TreeNode this[int index] => EnsureInitialized()[index];
 
-        public int Count
-        {
-            get
-            {
-                EnsureInitialized();
-                return _inner.Count;
-            }
-        }
+        public int Count => EnsureInitialized().Count;
 
         protected TreeNode Owner { get; }
 
-        public event NotifyCollectionChangedEventHandler CollectionChanged
+        public event NotifyCollectionChangedEventHandler? CollectionChanged
         {
-            add => _inner.CollectionChanged += value;
-            remove => _inner.CollectionChanged -= value;
+            add => EnsureInitialized().CollectionChanged += value;
+            remove => EnsureInitialized().CollectionChanged -= value;
         }
 
-        public event PropertyChangedEventHandler PropertyChanged
+        public event PropertyChangedEventHandler? PropertyChanged
         {
-            add => _inner.PropertyChanged += value;
-            remove => _inner.PropertyChanged -= value;
+            add => EnsureInitialized().PropertyChanged += value;
+            remove => EnsureInitialized().PropertyChanged -= value;
         }
 
         public virtual void Dispose()
@@ -58,21 +45,21 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public IEnumerator<TreeNode> GetEnumerator()
         {
-            EnsureInitialized();
-            return _inner.GetEnumerator();
+            return EnsureInitialized().GetEnumerator();
         }
 
         IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 
         protected abstract void Initialize(AvaloniaList<TreeNode> nodes);
 
-        private void EnsureInitialized()
+        private AvaloniaList<TreeNode> EnsureInitialized()
         {
             if (_inner is null)
             {
                 _inner = new AvaloniaList<TreeNode>();
                 Initialize(_inner);
             }
+            return _inner;
         }
     }
 }

+ 7 - 7
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@@ -6,8 +6,8 @@ namespace Avalonia.Diagnostics.ViewModels
 {
     internal class TreePageViewModel : ViewModelBase, IDisposable
     {
-        private TreeNode _selectedNode;
-        private ControlDetailsViewModel _details;
+        private TreeNode? _selectedNode;
+        private ControlDetailsViewModel? _details;
 
         public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
         {
@@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public TreeNode[] Nodes { get; protected set; }
 
-        public TreeNode SelectedNode
+        public TreeNode? SelectedNode
         {
             get => _selectedNode;
             private set
@@ -44,7 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public ControlDetailsViewModel Details
+        public ControlDetailsViewModel? Details
         {
             get => _details;
             private set
@@ -68,7 +68,7 @@ namespace Avalonia.Diagnostics.ViewModels
             _details?.Dispose();
         }
 
-        public TreeNode FindNode(IControl control)
+        public TreeNode? FindNode(IControl control)
         {
             foreach (var node in Nodes)
             {
@@ -104,7 +104,7 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        private void ExpandNode(TreeNode node)
+        private void ExpandNode(TreeNode? node)
         {
             if (node != null)
             {
@@ -113,7 +113,7 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        private TreeNode FindNode(TreeNode node, IControl control)
+        private TreeNode? FindNode(TreeNode node, IControl control)
         {
             if (node.Visual == control)
             {

+ 6 - 6
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs

@@ -1,16 +1,16 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
 using System.Runtime.CompilerServices;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
     internal class ViewModelBase : INotifyPropertyChanged
     {
-        private PropertyChangedEventHandler _propertyChanged;
+        private PropertyChangedEventHandler? _propertyChanged;
         private List<string> events = new List<string>();
 
-        public event PropertyChangedEventHandler PropertyChanged
+        public event PropertyChangedEventHandler? PropertyChanged
         {
             add { _propertyChanged += value; events.Add("added"); }
             remove { _propertyChanged -= value; events.Add("removed"); }
@@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels
         {
         }
 
-        protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+        protected bool RaiseAndSetIfChanged<T>([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!)
         {
             if (!EqualityComparer<T>.Default.Equals(field, value))
             {
@@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels
             return false;
         }
 
-        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
+        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!)
         {
             var e = new PropertyChangedEventArgs(propertyName);
             OnPropertyChanged(e);

+ 5 - 3
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@@ -7,7 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels
 {
     internal class VisualTreeNode : TreeNode
     {
-        public VisualTreeNode(IVisual visual, TreeNode parent)
+        public VisualTreeNode(IVisual visual, TreeNode? parent)
             : base(visual, parent)
         {
             Children = new VisualTreeNodeCollection(this, visual);
@@ -20,16 +20,18 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public bool IsInTemplate { get; private set; }
 
+        public override TreeNodeCollection Children { get; }
+
         public static VisualTreeNode[] Create(object control)
         {
             var visual = control as IVisual;
-            return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
+            return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty<VisualTreeNode>();
         }
 
         internal class VisualTreeNodeCollection : TreeNodeCollection
         {
             private readonly IVisual _control;
-            private IDisposable _subscription;
+            private IDisposable? _subscription;
 
             public VisualTreeNodeCollection(TreeNode owner, IVisual control)
                 : base(owner)

+ 9 - 5
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs

@@ -30,22 +30,26 @@ namespace Avalonia.Diagnostics.Views
             AvaloniaXamlLoader.Load(this);
         }
 
-        private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e)
+        private void HistoryChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
-            if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control)
+            if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IControl control)
             {
                 DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero);
             }
         }
 
-        private void InputKeyDown(object sender, KeyEventArgs e)
+        private void InputKeyDown(object? sender, KeyEventArgs e)
         {
-            var vm = (ConsoleViewModel)DataContext;
+            var vm = (ConsoleViewModel?)DataContext;
+            if (vm is null)
+            {
+                return;
+            }
 
             switch (e.Key)
             {
                 case Key.Enter:
-                    vm.Execute();
+                    _ = vm.Execute();
                     e.Handled = true;
                     break;
                 case Key.Up:

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs

@@ -53,7 +53,7 @@ namespace Avalonia.Diagnostics.Views
             }
         }
 
-        private void OnRecordedEventsChanged(object sender, NotifyCollectionChangedEventArgs e)
+        private void OnRecordedEventsChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
             if (sender is ObservableCollection<FiredEvent> events)
             {

+ 6 - 2
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs

@@ -27,7 +27,11 @@ namespace Avalonia.Diagnostics.Views
 
         public void ToggleConsole()
         {
-            var vm = (MainViewModel)DataContext;
+            var vm = (MainViewModel?)DataContext;
+            if (vm is null)
+            {
+                return;
+            }
 
             if (_consoleHeight == -1)
             {
@@ -54,7 +58,7 @@ namespace Avalonia.Diagnostics.Views
             AvaloniaXamlLoader.Load(this);
         }
 
-        private void PreviewKeyDown(object sender, KeyEventArgs e)
+        private void PreviewKeyDown(object? sender, KeyEventArgs e)
         {
             if (e.Key == Key.Escape)
             {

+ 22 - 11
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Views
     internal class MainWindow : Window, IStyleHost
     {
         private readonly IDisposable _keySubscription;
-        private TopLevel _root;
+        private TopLevel? _root;
 
         public MainWindow()
         {
@@ -26,7 +26,7 @@ namespace Avalonia.Diagnostics.Views
                 .Subscribe(RawKeyDown);
         }
 
-        public TopLevel Root
+        public TopLevel? Root
         {
             get => _root;
             set
@@ -43,7 +43,7 @@ namespace Avalonia.Diagnostics.Views
                     if (_root != null)
                     {
                         _root.Closed += RootClosed;
-                        DataContext = new MainViewModel(value);
+                        DataContext = new MainViewModel(_root);
                     }
                     else
                     {
@@ -53,15 +53,20 @@ namespace Avalonia.Diagnostics.Views
             }
         }
 
-        IStyleHost IStyleHost.StylingParent => null;
+        IStyleHost? IStyleHost.StylingParent => null;
 
         protected override void OnClosed(EventArgs e)
         {
             base.OnClosed(e);
             _keySubscription.Dispose();
-            _root.Closed -= RootClosed;
-            _root = null;
-            ((MainViewModel)DataContext)?.Dispose();
+
+            if (_root != null)
+            {
+                _root.Closed -= RootClosed;
+                _root = null;
+            }
+
+            ((MainViewModel?)DataContext)?.Dispose();
         }
 
         private void InitializeComponent()
@@ -71,12 +76,20 @@ namespace Avalonia.Diagnostics.Views
 
         private void RawKeyDown(RawKeyEventArgs e)
         {
+            var vm = (MainViewModel?)DataContext;
+            if (vm is null)
+            {
+                return;
+            }
+
             const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
 
             if (e.Modifiers == modifiers)
             {
+#pragma warning disable CS0618 // Type or member is obsolete
                 var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default;
-                
+#pragma warning restore CS0618 // Type or member is obsolete                
+
                 var control = Root.GetVisualsAt(point, x =>
                     {
                         if (x is AdornerLayer || !x.IsVisible) return false;
@@ -87,7 +100,6 @@ namespace Avalonia.Diagnostics.Views
 
                 if (control != null)
                 {
-                    var vm = (MainViewModel)DataContext;
                     vm.SelectControl((IControl)control);
                 }
             } 
@@ -97,12 +109,11 @@ namespace Avalonia.Diagnostics.Views
                 {
                     var enable = e.Key == Key.S;
 
-                    var vm = (MainViewModel)DataContext;
                     vm.EnableSnapshotStyles(enable);
                 }
             }
         }
 
-        private void RootClosed(object sender, EventArgs e) => Close();
+        private void RootClosed(object? sender, EventArgs e) => Close();
     }
 }

+ 14 - 10
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@@ -14,12 +14,13 @@ namespace Avalonia.Diagnostics.Views
     internal class TreePageView : UserControl
     {
         private readonly Panel _adorner;
-        private AdornerLayer _currentLayer;
+        private AdornerLayer? _currentLayer;
         private TreeView _tree;
 
         public TreePageView()
         {
             InitializeComponent();
+            _tree = this.FindControl<TreeView>("tree");
             _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
 
             _adorner = new Panel
@@ -37,9 +38,15 @@ namespace Avalonia.Diagnostics.Views
             };
         }
 
-        protected void AddAdorner(object sender, PointerEventArgs e)
+        protected void AddAdorner(object? sender, PointerEventArgs e)
         {
-            var node = (TreeNode)((Control)sender).DataContext;
+            var node = (TreeNode?)((Control)sender!).DataContext;
+            var vm = (TreePageViewModel?)DataContext;
+            if (node is null || vm is null)
+            {
+                return;
+            }
+
             var visual = (Visual)node.Visual;
 
             _currentLayer = AdornerLayer.GetAdornerLayer(visual);
@@ -53,8 +60,6 @@ namespace Avalonia.Diagnostics.Views
             _currentLayer.Children.Add(_adorner);
             AdornerLayer.SetAdornedElement(_adorner, visual);
 
-            var vm = (TreePageViewModel) DataContext;
-
             if (vm.MainView.ShouldVisualizeMarginPadding)
             {
                 var paddingBorder = (Border)_adorner.Children[0];
@@ -74,7 +79,7 @@ namespace Avalonia.Diagnostics.Views
             return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom);
         }
 
-        protected void RemoveAdorner(object sender, PointerEventArgs e)
+        protected void RemoveAdorner(object? sender, PointerEventArgs e)
         {
             foreach (var border in _adorner.Children.OfType<Border>())
             {
@@ -90,18 +95,17 @@ namespace Avalonia.Diagnostics.Views
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
-            _tree = this.FindControl<TreeView>("tree");
         }
 
-        private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e)
+        private void TreeViewItemMaterialized(object? sender, ItemContainerEventArgs e)
         {
             var item = (TreeViewItem)e.Containers[0].ContainerControl;
             item.TemplateApplied += TreeViewItemTemplateApplied;
         }
 
-        private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e)
+        private void TreeViewItemTemplateApplied(object? sender, TemplateAppliedEventArgs e)
         {
-            var item = (TreeViewItem)sender;
+            var item = (TreeViewItem)sender!;
 
             // This depends on the default tree item template.
             // We want to handle events in the item header but exclude events coming from children.

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Diagnostics
 
         private static void PrintVisualTree(IVisual visual, StringBuilder builder, int indent)
         {
-            Control control = visual as Control;
+            Control? control = visual as Control;
 
             builder.Append(Indent(indent - 1));