CmdPal: Removing Core projects (#45693)

Functionally, no differences.

- Removed Core projects.
- Core.Common => Microsoft.CmdPal.Common
- Core.ViewModels => Microsoft.CmdPal.UI.ViewModels

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
This commit is contained in:
Michael Jolley
2026-02-23 04:05:09 -08:00
committed by GitHub
parent 196b9305c3
commit 138c66c328
280 changed files with 473 additions and 825 deletions

View File

@@ -0,0 +1,285 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Buffers;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.Common;
using Microsoft.CmdPal.Common.Helpers;
namespace Microsoft.CmdPal.UI.ViewModels;
public abstract partial class ExtensionObjectViewModel : ObservableObject, IBatchUpdateTarget, IBackgroundPropertyChangedNotification
{
private const int InitialPropertyBatchingBufferSize = 16;
// Raised on the background thread before UI notifications. It's raised on the background thread to prevent
// blocking the COM proxy.
public event PropertyChangedEventHandler? PropertyChangedBackground;
private readonly ConcurrentQueue<string> _pendingProps = [];
private readonly TaskScheduler _uiScheduler;
private InterlockedBoolean _batchQueued;
public WeakReference<IPageContext> PageContext { get; private set; } = null!;
TaskScheduler IBatchUpdateTarget.UIScheduler => _uiScheduler;
void IBatchUpdateTarget.ApplyPendingUpdates() => ApplyPendingUpdates();
bool IBatchUpdateTarget.TryMarkBatchQueued() => _batchQueued.Set();
void IBatchUpdateTarget.ClearBatchQueued() => _batchQueued.Clear();
private protected ExtensionObjectViewModel(TaskScheduler scheduler)
{
if (this is not IPageContext)
{
throw new InvalidOperationException($"Constructor overload without IPageContext can only be used when the derived class implements IPageContext. Type: {GetType().FullName}");
}
_uiScheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler));
// Defer PageContext assignment - derived constructor MUST call InitializePageContext()
// or we set it lazily on first access
}
private protected ExtensionObjectViewModel(IPageContext context)
{
ArgumentNullException.ThrowIfNull(context);
PageContext = new WeakReference<IPageContext>(context);
_uiScheduler = context.Scheduler;
LogIfDefaultScheduler();
}
private protected ExtensionObjectViewModel(WeakReference<IPageContext> contextRef)
{
ArgumentNullException.ThrowIfNull(contextRef);
if (!contextRef.TryGetTarget(out var context))
{
throw new ArgumentException("IPageContext must be alive when creating view models.", nameof(contextRef));
}
PageContext = contextRef;
_uiScheduler = context.Scheduler;
LogIfDefaultScheduler();
}
protected void InitializeSelfAsPageContext()
{
if (this is not IPageContext self)
{
throw new InvalidOperationException("This method can only be called when the class implements IPageContext.");
}
PageContext = new WeakReference<IPageContext>(self);
}
private void LogIfDefaultScheduler()
{
if (_uiScheduler == TaskScheduler.Default)
{
CoreLogger.LogDebug($"ExtensionObjectViewModel created with TaskScheduler.Default. Type: {GetType().FullName}");
}
}
public virtual Task InitializePropertiesAsync()
=> Task.Run(SafeInitializePropertiesSynchronous);
public void SafeInitializePropertiesSynchronous()
{
try
{
InitializeProperties();
}
catch (Exception ex)
{
ShowException(ex);
}
}
public abstract void InitializeProperties();
protected void UpdateProperty(string propertyName) => MarkPropertyDirty(propertyName);
protected void UpdateProperty(string propertyName1, string propertyName2)
{
MarkPropertyDirty(propertyName1);
MarkPropertyDirty(propertyName2);
}
protected void UpdateProperty(params string[] propertyNames)
{
foreach (var p in propertyNames)
{
MarkPropertyDirty(p);
}
}
internal void MarkPropertyDirty(string? propertyName)
{
if (string.IsNullOrEmpty(propertyName))
{
return;
}
// We should re-consider if this worth deduping
_pendingProps.Enqueue(propertyName);
BatchUpdateManager.Queue(this);
}
public void ApplyPendingUpdates()
{
((IBatchUpdateTarget)this).ClearBatchQueued();
var buffer = ArrayPool<string>.Shared.Rent(InitialPropertyBatchingBufferSize);
var count = 0;
var transferred = false;
try
{
while (_pendingProps.TryDequeue(out var name))
{
if (count == buffer.Length)
{
var bigger = ArrayPool<string>.Shared.Rent(buffer.Length * 2);
Array.Copy(buffer, bigger, buffer.Length);
ArrayPool<string>.Shared.Return(buffer, clearArray: true);
buffer = bigger;
}
buffer[count++] = name;
}
if (count == 0)
{
return;
}
// 1) Background subscribers (must be raised before UI notifications).
var propertyChangedEventHandler = PropertyChangedBackground;
if (propertyChangedEventHandler is not null)
{
RaiseBackground(propertyChangedEventHandler, this, buffer, count);
}
// 2) UI-facing PropertyChanged: ALWAYS marshal to UI scheduler.
// Hand-off pooled buffer to UI task (UI task returns it).
//
// It would be lovely to do nothing if no one is actually listening on PropertyChanged,
// but ObservableObject doesn't expose that information.
_ = Task.Factory.StartNew(
static state =>
{
var p = (UiBatch)state!;
try
{
p.Owner.RaiseUi(p.Names, p.Count);
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to raise property change notifications on UI thread.", ex);
}
finally
{
ArrayPool<string>.Shared.Return(p.Names, clearArray: true);
}
},
new UiBatch(this, buffer, count),
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
_uiScheduler);
transferred = true;
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to apply pending property updates.", ex);
}
finally
{
if (!transferred)
{
ArrayPool<string>.Shared.Return(buffer, clearArray: true);
}
}
}
private void RaiseUi(string[] names, int count)
{
for (var i = 0; i < count; i++)
{
OnPropertyChanged(Args(names[i]));
}
}
private static void RaiseBackground(PropertyChangedEventHandler handlers, object sender, string[] names, int count)
{
try
{
for (var i = 0; i < count; i++)
{
handlers(sender, Args(names[i]));
}
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to raise PropertyChangedBackground notifications.", ex);
}
}
private sealed record UiBatch(ExtensionObjectViewModel Owner, string[] Names, int Count);
protected void ShowException(Exception ex, string? extensionHint = null)
{
if (PageContext.TryGetTarget(out var pageContext))
{
pageContext.ShowException(ex, extensionHint);
}
else
{
CoreLogger.LogError("Failed to show exception because PageContext is no longer available.", ex);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static PropertyChangedEventArgs Args(string name) => new(name);
protected void DoOnUiThread(Action action)
{
if (PageContext.TryGetTarget(out var pageContext))
{
Task.Factory.StartNew(
action,
CancellationToken.None,
TaskCreationOptions.None,
pageContext.Scheduler);
}
}
protected virtual void UnsafeCleanup()
{
// base doesn't do anything, but sub-classes should override this.
}
public virtual void SafeCleanup()
{
try
{
UnsafeCleanup();
}
catch (Exception ex)
{
CoreLogger.LogDebug(ex.ToString());
}
}
}