File: DocumentOutline\DocumentSymbolDataViewModel.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ozsccwvc_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Immutable;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.Editor.Wpf;
using Microsoft.VisualStudio.Imaging.Interop;
using Microsoft.VisualStudio.Text;
 
namespace Microsoft.VisualStudio.LanguageServices.DocumentOutline;
 
/// <summary>
/// A ViewModel over <see cref="DocumentSymbolData"/>. The only items that are mutable on this type are <see
/// cref="IsExpanded"/> and <see cref="IsSelected"/>. It is expected that these can be modified from any thread with
/// INotifyPropertyChanged notifications being marshalled to the correct thread by WPF if there needs to be a change
/// to the visual presentation.
/// </summary>
internal sealed class DocumentSymbolDataViewModel : INotifyPropertyChanged, IEquatable<DocumentSymbolDataViewModel>
{
    public DocumentSymbolData Data { get; }
    public ImmutableArray<DocumentSymbolDataViewModel> Children { get; }
 
    /// <summary>
    /// Necessary because we cannot convert to this type dynamically in WPF.
    /// </summary>
    public ImageMoniker ImageMoniker => Data.Glyph.GetImageMoniker();
 
    private bool _isExpanded = true;
    private bool _isSelected = false;
 
    public bool IsExpanded
    {
        get => _isExpanded;
        set => SetProperty(ref _isExpanded, value);
    }
 
    public bool IsSelected
    {
        get => _isSelected;
        set => SetProperty(ref _isSelected, value);
    }
 
    public DocumentSymbolDataViewModel(
        DocumentSymbolData data,
        ImmutableArray<DocumentSymbolDataViewModel> children)
    {
        Data = data;
        Children = children;
    }
 
    private static readonly PropertyChangedEventArgs _isExpandedPropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(IsExpanded));
    private static readonly PropertyChangedEventArgs _isSelectedPropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(IsSelected));
 
    private void NotifyPropertyChanged([CallerMemberName] string? propertyName = null)
        => PropertyChanged?.Invoke(this, propertyName switch
        {
            nameof(IsExpanded) => _isExpandedPropertyChangedEventArgs,
            nameof(IsSelected) => _isSelectedPropertyChangedEventArgs,
            _ => new PropertyChangedEventArgs(propertyName)
        });
 
    public event PropertyChangedEventHandler? PropertyChanged;
 
    private void SetProperty(ref bool field, bool value, [CallerMemberName] string propertyName = "")
    {
        // Note: we do not lock here. Worst case is that we fire multiple
        //       NotifyPropertyChanged events which WPF can handle.
        if (field == value)
            return;
 
        field = value;
        NotifyPropertyChanged(propertyName);
    }
 
    public override bool Equals(object obj)
        => Equals(obj as DocumentSymbolDataViewModel);
 
    public bool Equals(DocumentSymbolDataViewModel? other)
    {
        if (other is null)
            return false;
 
        if (ReferenceEquals(this, other))
            return true;
 
        // If two view models are in the same location (across edits), we consider them the same.  We want to treat
        // things like name edits as just mutating a model, not producing a new one.
        var translatedRangeSpan = this.Data.RangeSpan.TranslateTo(other.Data.RangeSpan.Snapshot, SpanTrackingMode.EdgeInclusive);
        return translatedRangeSpan == other.Data.RangeSpan;
    }
 
    public override int GetHashCode()
        => Data.GetHashCode();
}