sources/Google.Solutions.IapDesktop.Application/ToolWindows/ProjectExplorer/ProjectExplorerViewModel.Nodes.cs (421 lines of code) (raw):
//
// Copyright 2021 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Solutions.Apis;
using Google.Solutions.Apis.Locator;
using Google.Solutions.Common.Util;
using Google.Solutions.IapDesktop.Application.Windows;
using Google.Solutions.IapDesktop.Core.ProjectModel;
using Google.Solutions.Mvvm.Binding;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Google.Solutions.IapDesktop.Application.ToolWindows.ProjectExplorer
{
public partial class ProjectExplorerViewModel
{
public abstract class ViewModelNode : ViewModelBase
{
private bool isExpanded;
private readonly int defaultImageIndex;
//
// Child nodes, loaded lazily.
//
private RangeObservableCollection<ViewModelNode>? children;
private RangeObservableCollection<ViewModelNode>? filteredChildren;
//-----------------------------------------------------------------
// Properties.
//-----------------------------------------------------------------
internal ViewModelNode? Parent { get; }
internal abstract bool CanReload { get; }
protected abstract ProjectExplorerViewModel ViewModel { get; }
public abstract IProjectModelNode ModelNode { get; }
public ComputeEngineLocator? Locator { get; }
public string Text { get; }
public bool IsLeaf { get; }
public virtual int ImageIndex
{
get => this.defaultImageIndex;
}
public bool IsExpanded
{
get => this.isExpanded;
set
{
this.isExpanded = value;
RaisePropertyChange();
OnExpandedChanged();
}
}
internal virtual bool IsLoaded
{
get => this.children != null;
}
/// <summary>
/// List all children that have been loaded, ignoring any filter.
/// </summary>
internal IEnumerable<ViewModelNode> LoadedChildren
{
get => this.children ?? Enumerable.Empty<ViewModelNode>();
}
/// <summary>
/// List all descendents that have been loaded, ignoring any filter.
/// </summary>
internal IEnumerable<ViewModelNode> LoadedDescendents
{
get
{
var accumulator = new List<ViewModelNode>();
foreach (var child in this.LoadedChildren)
{
accumulator.Add(child);
accumulator.AddRange(child.LoadedDescendents);
}
return accumulator;
}
}
//-----------------------------------------------------------------
// Children.
//-----------------------------------------------------------------
protected virtual void OnExpandedChanged() { }
protected virtual IEnumerable<ViewModelNode> ApplyFilter(
RangeObservableCollection<ViewModelNode> allNodes)
{
return allNodes;
}
/// <summary>
/// List all children that match the current filter. Causes
/// children to be loaded if necessary.
/// </summary>
public async Task<ObservableCollection<ViewModelNode>> GetFilteredChildrenAsync(
bool forceReload)
{
Debug.Assert(!((Control)this.ViewModel.View!).InvokeRequired);
Debug.Assert(!this.IsLeaf);
if (this.children == null)
{
Debug.Assert(this.filteredChildren == null);
//
// Load lazily. No locking required as we're
// operating on the UI thread.
//
var loadedNodes = await LoadChildrenAsync(forceReload)
.ConfigureAwait(true);
this.children = new RangeObservableCollection<ViewModelNode>();
this.filteredChildren = new RangeObservableCollection<ViewModelNode>();
this.children.AddRange(loadedNodes);
ReapplyFilter();
}
else if (forceReload)
{
Debug.Assert(this.filteredChildren != null);
var loadedNodes = await LoadChildrenAsync(forceReload)
.ConfigureAwait(true);
this.children.Clear();
this.children.AddRange(loadedNodes);
ReapplyFilter();
}
else
{
// Use cached copy.
}
Debug.Assert(this.filteredChildren != null);
Debug.Assert(this.children != null);
return this.filteredChildren!;
}
/// <summary>
/// Load children in a job.
/// </summary>
protected async Task<IEnumerable<ViewModelNode>> LoadChildrenAsync(
bool forceReload)
{
using (this.ViewModel.EnableLoadingStatus())
{
//
// Wrap loading task in a job since it might kick of
// I/O (if data has not been cached yet).
//
return await this.ViewModel.jobService.RunAsync(
new JobDescription(
$"Loading {this.Text}...",
JobUserFeedbackType.BackgroundFeedback),
token => LoadChildrenAsync(forceReload, token))
.ConfigureAwait(true);
}
}
/// <summary>
/// Load children.
/// </summary>
protected abstract Task<IEnumerable<ViewModelNode>> LoadChildrenAsync(
bool forceReload,
CancellationToken token);
internal bool DebugIsValidNode(ViewModelNode node)
{
if (node == this)
{
return true;
}
else if (this.filteredChildren == null)
{
return false;
}
else
{
return this.filteredChildren.Any(n => n.DebugIsValidNode(n));
}
}
internal void ReapplyFilter()
{
Debug.Assert(this.IsLoaded);
if (this.IsLeaf)
{
return;
}
Debug.Assert(this.children != null);
Debug.Assert(this.filteredChildren != null);
//
// Avoid clearing and re-applying filter it's not really
// necessary. Excessive re-binding can otherwise cause
// significant CPU load.
//
var newFilteredNodes = ApplyFilter(this.children!);
if (!this.filteredChildren!.SequenceEqual(newFilteredNodes))
{
this.filteredChildren!.Clear();
this.filteredChildren.AddRange(newFilteredNodes);
}
foreach (var n in this.children.Where(n => n.IsLoaded))
{
n.ReapplyFilter();
}
}
//-----------------------------------------------------------------
// Ctor.
//-----------------------------------------------------------------
protected ViewModelNode(
ViewModelNode? parent,
ComputeEngineLocator? locator,
string text,
bool isLeaf,
int defaultImageIndex)
{
this.Parent = parent;
this.Locator = locator;
this.Text = text;
this.IsLeaf = isLeaf;
this.defaultImageIndex = defaultImageIndex;
}
}
public class CloudViewModelNode : ViewModelNode
{
private const int DefaultIconIndex = 0;
private IProjectModelCloudNode? cloudNode; // Loaded lazily.
protected override ProjectExplorerViewModel ViewModel { get; }
public CloudViewModelNode(
ProjectExplorerViewModel viewModel)
: base(
null,
null,
"Google Cloud",
false,
DefaultIconIndex)
{
this.ViewModel = viewModel;
}
public override IProjectModelNode ModelNode
{
get
{
Invariant.ExpectNotNull(this.cloudNode, "Loading completed");
return this.cloudNode!;
}
}
internal override bool CanReload
{
get => true;
}
protected override async Task<IEnumerable<ViewModelNode>> LoadChildrenAsync(
bool forceReload,
CancellationToken token)
{
this.cloudNode = await this.ViewModel.workspace
.GetRootNodeAsync(forceReload, token)
.ConfigureAwait(true);
var children = new List<ViewModelNode>();
children.AddRange(this.cloudNode.Projects
.Select(m => new ProjectViewModelNode(
this.ViewModel,
this,
m))
.OrderBy(n => n.Text));
return children;
}
}
internal class ProjectViewModelNode : ViewModelNode
{
private const int DefaultIconIndex = 1;
internal IProjectModelProjectNode ProjectNode { get; }
protected override ProjectExplorerViewModel ViewModel { get; }
private static string CreateDisplayName(IProjectModelProjectNode node)
{
if (!node.IsAccesible)
{
return $"inaccessible project ({node.Project.Name})";
}
else if (node.Project.Name == node.DisplayName)
{
return node.Project.Name;
}
else
{
return $"{node.DisplayName} ({node.Project.Name})";
}
}
public ProjectViewModelNode(
ProjectExplorerViewModel viewModel,
CloudViewModelNode parent,
IProjectModelProjectNode modelNode)
: base(
parent,
modelNode.Project,
CreateDisplayName(modelNode),
false,
DefaultIconIndex)
{
this.ProjectNode = modelNode;
this.ViewModel = viewModel;
this.IsExpanded = !viewModel.settings.CollapsedProjects.Contains(modelNode.Project);
}
protected override void OnExpandedChanged()
{
if (this.IsExpanded)
{
this.ViewModel.settings.CollapsedProjects.Remove(this.ProjectNode.Project);
}
else
{
this.ViewModel.settings.CollapsedProjects.Add(this.ProjectNode.Project);
}
base.OnExpandedChanged();
}
public override IProjectModelNode ModelNode
{
get => this.ProjectNode;
}
internal override bool CanReload
{
get => true;
}
protected override async Task<IEnumerable<ViewModelNode>> LoadChildrenAsync(
bool forceReload,
CancellationToken token)
{
try
{
var zones = await this.ViewModel.workspace.GetZoneNodesAsync(
this.ProjectNode.Project,
forceReload,
token)
.ConfigureAwait(true);
return zones
.Select(z => new ZoneViewModelNode(this.ViewModel, this, z))
.Cast<ViewModelNode>();
}
catch (Exception e) when (
e.Is<ResourceAccessDeniedException>() ||
e.Is<ResourceNotFoundException>())
{
//
// Letting these exception propagate could cause a flurry
// of error messages when multiple projects have become
// inaccessible. So it's best to interpret this error as
// "cannot list any VMs" and return an empty list.
//
return Enumerable.Empty<ZoneViewModelNode>();
}
}
}
internal class ZoneViewModelNode : ViewModelNode
{
private const int DefaultIconIndex = 3;
internal IProjectModelZoneNode ZoneNode { get; }
protected override ProjectExplorerViewModel ViewModel { get; }
public ZoneViewModelNode(
ProjectExplorerViewModel viewModel,
ProjectViewModelNode parent,
IProjectModelZoneNode modelNode)
: base(
parent,
modelNode.Zone,
modelNode.DisplayName,
false,
DefaultIconIndex)
{
this.ZoneNode = modelNode;
this.ViewModel = viewModel;
this.IsExpanded = true;
}
public override IProjectModelNode ModelNode
{
get => this.ZoneNode;
}
internal override bool CanReload
{
get => false;
}
protected override Task<IEnumerable<ViewModelNode>> LoadChildrenAsync(
bool forceReload,
CancellationToken token)
{
return Task.FromResult(this.ZoneNode
.Instances
.Select(i => new InstanceViewModelNode(this.ViewModel, this, i))
.Cast<ViewModelNode>());
}
protected override IEnumerable<ViewModelNode> ApplyFilter(
RangeObservableCollection<ViewModelNode> allNodes)
{
return allNodes
.Cast<InstanceViewModelNode>()
.Where(i => this.ViewModel.InstanceFilter == null ||
i.InstanceNode.DisplayName.IndexOf(
this.ViewModel.InstanceFilter,
StringComparison.OrdinalIgnoreCase) >= 0)
.Where(i => (i.InstanceNode.OperatingSystem &
this.ViewModel.OperatingSystemsFilter) != 0);
}
}
internal class InstanceViewModelNode : ViewModelNode
{
internal const int WindowsDisconnectedIconIndex = 4;
internal const int WindowsConnectedIconIndex = 5;
internal const int StoppedIconIndex = 6;
internal const int LinuxDisconnectedIconIndex = 7;
internal const int LinuxConnectedIconIndex = 8;
private bool isConnected = false;
internal IProjectModelInstanceNode InstanceNode { get; }
protected override ProjectExplorerViewModel ViewModel { get; }
public InstanceViewModelNode(
ProjectExplorerViewModel viewModel,
ZoneViewModelNode parent,
IProjectModelInstanceNode modelNode)
: base(
parent,
modelNode.Instance,
modelNode.DisplayName,
true,
-1)
{
this.InstanceNode = modelNode;
this.ViewModel = viewModel;
this.IsConnected = viewModel.sessionBroker.IsConnected(modelNode.Instance);
}
public override IProjectModelNode ModelNode
{
get => this.InstanceNode;
}
internal override bool CanReload
{
get => false;
}
public override int ImageIndex
{
get
{
if (this.IsConnected)
{
return this.InstanceNode.OperatingSystem == OperatingSystems.Windows
? WindowsConnectedIconIndex
: LinuxConnectedIconIndex;
}
else if (!this.InstanceNode.IsRunning)
{
return StoppedIconIndex;
}
else
{
return this.InstanceNode.OperatingSystem == OperatingSystems.Windows
? WindowsDisconnectedIconIndex
: LinuxDisconnectedIconIndex;
}
}
}
public bool IsConnected
{
get => this.isConnected;
set
{
this.isConnected = value;
RaisePropertyChange();
RaisePropertyChange((InstanceViewModelNode n) => n.ImageIndex);
}
}
internal override bool IsLoaded
{
get => true;
}
protected override Task<IEnumerable<ViewModelNode>> LoadChildrenAsync(
bool forceReload,
CancellationToken token)
{
Debug.Fail("Should not be called since this is a leaf node");
return Task.FromResult(Enumerable.Empty<ViewModelNode>());
}
}
}
}