mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-02-10 04:03:41 +01:00
Add TryGetSource extension methods for WeakReference<View> access (#4694)
* Initial plan * CommandContext infrastructure: WeakReference and extension methods Co-authored-by: tig <585482+tig@users.noreply.github.com> * Align CommandContext infrastructure with PR #4620 - Update ViewExtensions.cs to use Terminal.Gui.ViewBase namespace - Update WeakReferenceExtensions.cs to match PR #4620 format - Change CommandContext constructor to take WeakReference<View>? directly - Add CommandContext.ToString() implementation - Update ICommandContext.Source documentation - Update View.Command.cs to explicitly create WeakReference - Update Shortcut.cs to handle WeakReference Source - Update ComboBox.cs Source comparison - Update Dialog.cs and DialogTResult.cs pattern matching - Update MenuBar.cs and PopoverMenu.cs to work with WeakReference - Update ScrollSlider.cs and OptionSelector.cs Remaining: Fix Examples code and tests Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix Examples code for WeakReference Source - Update UICatalogRunnable.cs to use TryGetTarget for Data access - Update Menus.cs to use ToIdentifyingString() and TryGetTarget - Update MouseTester.cs to use ToIdentifyingString() for all Source.Id access All Examples now build successfully Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update tests for WeakReference Source - Fix CommandContextTests.cs to wrap Source in WeakReference - Fix InputBindingTests.cs to wrap Source in WeakReference - Update assertions to use TryGetTarget for safe access - All CommandContext and InputBinding tests pass Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add TryGetSource extension methods and replace usage - Add TryGetSource extension to WeakReferenceExtensions.cs - Add CommandContextExtensions.cs with TryGetSource for ICommandContext - Replace 14 instances of Source?.TryGetTarget pattern across codebase - Updated Terminal.Gui files: ComboBox, Dialog, DialogTResult, MenuBar, PopoverMenu, OptionSelector, Shortcut - Updated Examples files: UICatalogRunnable, Menus - All files build successfully Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add tests for TryGetSource extension methods - Add 6 tests for TryGetSource extension methods - Test WeakReference<View>.TryGetSource with valid/null references - Test ICommandContext.TryGetSource with valid/null contexts - Test pattern matching usage with TryGetSource - All 23 CommandContext tests pass - Full test suite: 15,094 passed Co-authored-by: tig <585482+tig@users.noreply.github.com> * code cleanup --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
@@ -38,13 +38,12 @@ public class Menus : Scenario
|
||||
SchemeName = "Runnable",
|
||||
Source = new ListWrapper<string> (eventSource)
|
||||
};
|
||||
eventLog.Border!.Thickness = new (0, 1, 0, 0);
|
||||
eventLog.Border!.Thickness = new Thickness (0, 1, 0, 0);
|
||||
|
||||
MenuHost menuHostView = new ()
|
||||
{
|
||||
Id = "menuHostView",
|
||||
Title = $"Menu Host - Use {PopoverMenu.DefaultKey} for Popover Menu",
|
||||
|
||||
X = 0,
|
||||
Y = 0,
|
||||
Width = Dim.Fill ()! - Dim.Width (eventLog),
|
||||
@@ -72,8 +71,9 @@ public class Menus : Scenario
|
||||
return;
|
||||
}
|
||||
|
||||
Logging.Debug ($"{sender.Id} Accepting: {args.Context?.Source?.Title}");
|
||||
eventSource.Add ($"{sender.Id} Accepting: {args.Context?.Source?.Title}: ");
|
||||
string sourceTitle = args.Context?.Source.ToIdentifyingString () ?? "(null)";
|
||||
Logging.Debug ($"{sender.Id} Accepting: {sourceTitle}");
|
||||
eventSource.Add ($"{sender.Id} Accepting: {sourceTitle}: ");
|
||||
eventLog.MoveDown ();
|
||||
};
|
||||
|
||||
@@ -84,8 +84,14 @@ public class Menus : Scenario
|
||||
return;
|
||||
}
|
||||
|
||||
Logging.Debug ($"{sender.Id} Accepted: {args.Context?.Source?.Text}");
|
||||
eventSource.Add ($"{sender.Id} Accepted: {args.Context?.Source?.Text}: ");
|
||||
var sourceText = "(null)";
|
||||
|
||||
if (args.Context?.TryGetSource (out View? sourceView) == true)
|
||||
{
|
||||
sourceText = sourceView.Text;
|
||||
}
|
||||
Logging.Debug ($"{sender.Id} Accepted: {sourceText}");
|
||||
eventSource.Add ($"{sender.Id} Accepted: {sourceText}: ");
|
||||
eventLog.MoveDown ();
|
||||
};
|
||||
|
||||
@@ -106,8 +112,7 @@ public class Menus : Scenario
|
||||
CanFocus = true;
|
||||
BorderStyle = LineStyle.Dashed;
|
||||
|
||||
AddCommand (
|
||||
Command.Context,
|
||||
AddCommand (Command.Context,
|
||||
_ =>
|
||||
{
|
||||
ContextMenu?.MakeVisible ();
|
||||
@@ -132,20 +137,9 @@ public class Menus : Scenario
|
||||
|
||||
//MouseBindings.ReplaceCommands (MouseFlags.LeftButtonClicked, Command.Cancel);
|
||||
|
||||
Label lastCommandLabel = new ()
|
||||
{
|
||||
Title = "_Last Command:",
|
||||
X = 15,
|
||||
Y = 10
|
||||
};
|
||||
Label lastCommandLabel = new () { Title = "_Last Command:", X = 15, Y = 10 };
|
||||
|
||||
View lastCommandText = new ()
|
||||
{
|
||||
X = Pos.Right (lastCommandLabel) + 1,
|
||||
Y = Pos.Top (lastCommandLabel),
|
||||
Height = Dim.Auto (),
|
||||
Width = Dim.Auto ()
|
||||
};
|
||||
View lastCommandText = new () { X = Pos.Right (lastCommandLabel) + 1, Y = Pos.Top (lastCommandLabel), Height = Dim.Auto (), Width = Dim.Auto () };
|
||||
|
||||
Add (lastCommandLabel, lastCommandText);
|
||||
|
||||
@@ -161,8 +155,7 @@ public class Menus : Scenario
|
||||
AddCommand (Command.SaveAs, HandleCommand);
|
||||
HotKeyBindings.Add (Key.A.WithCtrl, Command.SaveAs);
|
||||
|
||||
AddCommand (
|
||||
Command.Quit,
|
||||
AddCommand (Command.Quit,
|
||||
_ =>
|
||||
{
|
||||
Logging.Debug ("MenuHost Command.Quit - RequestStop");
|
||||
@@ -190,28 +183,17 @@ public class Menus : Scenario
|
||||
App?.Keyboard.KeyBindings.Remove (Key.F5);
|
||||
App?.Keyboard.KeyBindings.Add (Key.F5, this, Command.Edit);
|
||||
|
||||
var menuBar = new MenuBar
|
||||
{
|
||||
Title = "MenuHost MenuBar"
|
||||
};
|
||||
var menuBar = new MenuBar { Title = "MenuHost MenuBar" };
|
||||
MenuHost host = this;
|
||||
menuBar.EnableForDesign (ref host);
|
||||
|
||||
Add (menuBar);
|
||||
|
||||
Label lastAcceptedLabel = new ()
|
||||
{
|
||||
Title = "Last Accepted:",
|
||||
X = Pos.Left (lastCommandLabel),
|
||||
Y = Pos.Bottom (lastCommandLabel)
|
||||
};
|
||||
Label lastAcceptedLabel = new () { Title = "Last Accepted:", X = Pos.Left (lastCommandLabel), Y = Pos.Bottom (lastCommandLabel) };
|
||||
|
||||
View lastAcceptedText = new ()
|
||||
{
|
||||
X = Pos.Right (lastAcceptedLabel) + 1,
|
||||
Y = Pos.Top (lastAcceptedLabel),
|
||||
Height = Dim.Auto (),
|
||||
Width = Dim.Auto ()
|
||||
X = Pos.Right (lastAcceptedLabel) + 1, Y = Pos.Top (lastAcceptedLabel), Height = Dim.Auto (), Width = Dim.Auto ()
|
||||
};
|
||||
|
||||
Add (lastAcceptedLabel, lastAcceptedText);
|
||||
@@ -222,13 +204,11 @@ public class Menus : Scenario
|
||||
// CB.
|
||||
// So that is needed is to mirror the two check boxes.
|
||||
var autoSaveMenuItemCb = menuBar.GetMenuItemsWithTitle ("_Auto Save").FirstOrDefault ()?.CommandView as CheckBox;
|
||||
Debug.Assert (autoSaveMenuItemCb is not null);
|
||||
Debug.Assert (autoSaveMenuItemCb is { });
|
||||
|
||||
CheckBox autoSaveStatusCb = new ()
|
||||
{
|
||||
Title = "AutoSave Status (MenuItem Binding to F10)",
|
||||
X = Pos.Left (lastAcceptedLabel),
|
||||
Y = Pos.Bottom (lastAcceptedLabel)
|
||||
Title = "AutoSave Status (MenuItem Binding to F10)", X = Pos.Left (lastAcceptedLabel), Y = Pos.Bottom (lastAcceptedLabel)
|
||||
};
|
||||
|
||||
autoSaveStatusCb.ValueChanged += (_, _) => { autoSaveMenuItemCb!.Value = autoSaveStatusCb.Value; };
|
||||
@@ -243,43 +223,42 @@ public class Menus : Scenario
|
||||
// If the user clicks on the MenuItem, Accept will be raised.
|
||||
CheckBox enableOverwriteStatusCb = new ()
|
||||
{
|
||||
Title = "Enable Overwrite (View Binding to Ctrl+W)",
|
||||
X = Pos.Left (autoSaveStatusCb),
|
||||
Y = Pos.Bottom (autoSaveStatusCb)
|
||||
Title = "Enable Overwrite (View Binding to Ctrl+W)", X = Pos.Left (autoSaveStatusCb), Y = Pos.Bottom (autoSaveStatusCb)
|
||||
};
|
||||
|
||||
// The source of truth is our status CB; any time it changes, update the menu item
|
||||
var enableOverwriteMenuItemCb = menuBar.GetMenuItemsWithTitle ("Overwrite").FirstOrDefault ()?.CommandView as CheckBox;
|
||||
|
||||
enableOverwriteStatusCb.ValueChanged += (_, _) =>
|
||||
{
|
||||
if (enableOverwriteMenuItemCb is not null)
|
||||
{
|
||||
enableOverwriteMenuItemCb.Value = enableOverwriteStatusCb.Value;
|
||||
}
|
||||
};
|
||||
{
|
||||
if (enableOverwriteMenuItemCb is { })
|
||||
{
|
||||
enableOverwriteMenuItemCb.Value = enableOverwriteStatusCb.Value;
|
||||
}
|
||||
};
|
||||
|
||||
menuBar.Accepted += (_, args) =>
|
||||
{
|
||||
if (args.Context?.Source is not MenuItem mi || mi.CommandView != enableOverwriteMenuItemCb)
|
||||
if (!(args.Context?.TryGetSource (out View? sourceView) == true)
|
||||
|| sourceView is not MenuItem mi
|
||||
|| mi.CommandView != enableOverwriteMenuItemCb)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logging.Debug ($"menuBar.Accepted: {args.Context.Source?.Title}");
|
||||
Logging.Debug ($"menuBar.Accepted: {args.Context?.Source.ToIdentifyingString ()}");
|
||||
|
||||
// Set Cancel to true to stop propagation of Accepting to superview
|
||||
args.Handled = true;
|
||||
|
||||
// Since overwrite uses a MenuItem.Command the menu item CB is the source of truth
|
||||
enableOverwriteStatusCb.Value = ((CheckBox)mi.CommandView).Value;
|
||||
lastAcceptedText.Text = args.Context?.Source?.Title!;
|
||||
lastAcceptedText.Text = sourceView.Title;
|
||||
};
|
||||
|
||||
HotKeyBindings.Add (Key.W.WithCtrl, Command.EnableOverwrite);
|
||||
|
||||
AddCommand (
|
||||
Command.EnableOverwrite,
|
||||
AddCommand (Command.EnableOverwrite,
|
||||
ctx =>
|
||||
{
|
||||
// The command was invoked. Toggle the status Cb.
|
||||
@@ -297,41 +276,40 @@ public class Menus : Scenario
|
||||
// If the user clicks on the MenuItem, Accept will be raised.
|
||||
CheckBox editModeStatusCb = new ()
|
||||
{
|
||||
Title = "EditMode (App Binding to F5)",
|
||||
X = Pos.Left (enableOverwriteStatusCb),
|
||||
Y = Pos.Bottom (enableOverwriteStatusCb)
|
||||
Title = "EditMode (App Binding to F5)", X = Pos.Left (enableOverwriteStatusCb), Y = Pos.Bottom (enableOverwriteStatusCb)
|
||||
};
|
||||
|
||||
// The source of truth is our status CB; any time it changes, update the menu item
|
||||
var editModeMenuItemCb = menuBar.GetMenuItemsWithTitle ("EditMode").FirstOrDefault ()?.CommandView as CheckBox;
|
||||
|
||||
editModeStatusCb.ValueChanged += (_, _) =>
|
||||
{
|
||||
if (editModeMenuItemCb is not null)
|
||||
{
|
||||
editModeMenuItemCb.Value = editModeStatusCb.Value;
|
||||
}
|
||||
};
|
||||
{
|
||||
if (editModeMenuItemCb is { })
|
||||
{
|
||||
editModeMenuItemCb.Value = editModeStatusCb.Value;
|
||||
}
|
||||
};
|
||||
|
||||
menuBar.Accepted += (_, args) =>
|
||||
{
|
||||
if (args.Context?.Source is not MenuItem mi || mi.CommandView != editModeMenuItemCb)
|
||||
if (!(args.Context?.TryGetSource (out View? sourceView) == true)
|
||||
|| sourceView is not MenuItem mi
|
||||
|| mi.CommandView != editModeMenuItemCb)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logging.Debug ($"menuBar.Accepted: {args.Context.Source?.Title}");
|
||||
Logging.Debug ($"menuBar.Accepted: {args.Context?.Source.ToIdentifyingString ()}");
|
||||
|
||||
// Set Cancel to true to stop propagation of Accepting to superview
|
||||
args.Handled = true;
|
||||
|
||||
// Since overwrite uses a MenuItem.Command the menu item CB is the source of truth
|
||||
editModeMenuItemCb.Value = ((CheckBox)mi.CommandView).Value;
|
||||
lastAcceptedText.Text = args.Context?.Source?.Title!;
|
||||
lastAcceptedText.Text = sourceView.Title;
|
||||
};
|
||||
|
||||
AddCommand (
|
||||
Command.Edit,
|
||||
AddCommand (Command.Edit,
|
||||
ctx =>
|
||||
{
|
||||
// The command was invoked. Toggle the status Cb.
|
||||
@@ -343,11 +321,7 @@ public class Menus : Scenario
|
||||
Add (editModeStatusCb);
|
||||
|
||||
// Set up the Context Menu
|
||||
ContextMenu = new ()
|
||||
{
|
||||
Title = "ContextMenu",
|
||||
Id = "ContextMenu"
|
||||
};
|
||||
ContextMenu = new PopoverMenu { Title = "ContextMenu", Id = "ContextMenu" };
|
||||
|
||||
ContextMenu.EnableForDesign (ref host);
|
||||
App?.Popover?.Register (ContextMenu);
|
||||
@@ -359,25 +333,22 @@ public class Menus : Scenario
|
||||
// we need to subscribe to the ContextMenu's Accepted event.
|
||||
ContextMenu!.Accepted += (_, args) =>
|
||||
{
|
||||
Logging.Debug ($"ContextMenu.Accepted: {args.Context?.Source?.Title}");
|
||||
Logging.Debug ($"ContextMenu.Accepted: {args.Context?.Source.ToIdentifyingString ()}");
|
||||
|
||||
// Forward the event to the MenuHost
|
||||
if (args.Context is not null)
|
||||
if (args.Context is { })
|
||||
{
|
||||
//InvokeCommand (args.Context.Command);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a button to open the contextmenu
|
||||
var openBtn = new Button
|
||||
{
|
||||
X = Pos.Center (), Y = 4, Text = "_Open Menu", IsDefault = true
|
||||
};
|
||||
var openBtn = new Button { X = Pos.Center (), Y = 4, Text = "_Open Menu", IsDefault = true };
|
||||
|
||||
openBtn.Accepting += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
Logging.Trace ($"openBtn.Accepting - Sending F9. {e.Context?.Source?.Title}");
|
||||
Logging.Trace ($"openBtn.Accepting - Sending F9. {e.Context?.Source.ToIdentifyingString ()}");
|
||||
NewKeyDownEvent (menuBar.Key);
|
||||
};
|
||||
|
||||
@@ -409,7 +380,7 @@ public class Menus : Scenario
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose (bool disposing)
|
||||
{
|
||||
if (ContextMenu is not null)
|
||||
if (ContextMenu is { })
|
||||
{
|
||||
ContextMenu.Dispose ();
|
||||
ContextMenu = null;
|
||||
@@ -428,22 +399,20 @@ public class Menus : Scenario
|
||||
// Configure Serilog to write logs to a file
|
||||
_logLevelSwitch.MinimumLevel = LogEventLevel.Verbose;
|
||||
|
||||
Log.Logger = new LoggerConfiguration ()
|
||||
.MinimumLevel.ControlledBy (_logLevelSwitch)
|
||||
.Enrich.FromLogContext () // Enables dynamic enrichment
|
||||
.WriteTo.Debug ()
|
||||
.WriteTo.File (
|
||||
_logFilePath,
|
||||
rollingInterval: RollingInterval.Day,
|
||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.CreateLogger ();
|
||||
Log.Logger = new LoggerConfiguration ().MinimumLevel.ControlledBy (_logLevelSwitch)
|
||||
.Enrich.FromLogContext () // Enables dynamic enrichment
|
||||
.WriteTo.Debug ()
|
||||
.WriteTo.File (_logFilePath,
|
||||
rollingInterval: RollingInterval.Day,
|
||||
outputTemplate:
|
||||
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.CreateLogger ();
|
||||
|
||||
// Create a logger factory compatible with Microsoft.Extensions.Logging
|
||||
using ILoggerFactory loggerFactory = LoggerFactory.Create (builder =>
|
||||
{
|
||||
builder
|
||||
.AddSerilog (dispose: true) // Integrate Serilog with ILogger
|
||||
.SetMinimumLevel (LogLevel.Trace); // Set minimum log level
|
||||
builder.AddSerilog (dispose: true) // Integrate Serilog with ILogger
|
||||
.SetMinimumLevel (LogLevel.Trace); // Set minimum log level
|
||||
});
|
||||
|
||||
// Get an ILogger instance
|
||||
|
||||
@@ -268,63 +268,63 @@ public class MouseTester : Scenario
|
||||
|
||||
demo.Activating += (_, args) =>
|
||||
{
|
||||
commandLogList.Add ($"{args.Context!.Source!.Id}:{args.Context!.Command}");
|
||||
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
|
||||
commandLog.MoveEnd ();
|
||||
args.Handled = true;
|
||||
};
|
||||
|
||||
demo.Accepting += (_, args) =>
|
||||
{
|
||||
commandLogList.Add ($"{args.Context!.Source!.Id}:{args.Context!.Command}");
|
||||
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
|
||||
commandLog.MoveEnd ();
|
||||
args.Handled = true;
|
||||
};
|
||||
|
||||
demo.CommandNotBound += (_, args) =>
|
||||
{
|
||||
commandLogList.Add ($"{args.Context!.Source!.Id}:{args.Context!.Command}");
|
||||
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
|
||||
commandLog.MoveEnd ();
|
||||
args.Handled = true;
|
||||
};
|
||||
|
||||
demoInPadding.Activating += (_, args) =>
|
||||
{
|
||||
commandLogList.Add ($"{args.Context!.Source!.Id}:{args.Context!.Command}");
|
||||
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
|
||||
commandLog.MoveEnd ();
|
||||
args.Handled = true;
|
||||
};
|
||||
|
||||
demoInPadding.Accepting += (_, args) =>
|
||||
{
|
||||
commandLogList.Add ($"{args.Context!.Source!.Id}:{args.Context!.Command}");
|
||||
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
|
||||
commandLog.MoveEnd ();
|
||||
args.Handled = true;
|
||||
};
|
||||
|
||||
sub1.Activating += (_, args) =>
|
||||
{
|
||||
commandLogList.Add ($"{args.Context!.Source!.Id}:{args.Context!.Command}");
|
||||
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
|
||||
commandLog.MoveEnd ();
|
||||
args.Handled = true;
|
||||
};
|
||||
|
||||
sub1.Accepting += (_, args) =>
|
||||
{
|
||||
commandLogList.Add ($"{args.Context!.Source!.Id}:{args.Context!.Command}");
|
||||
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
|
||||
commandLog.MoveEnd ();
|
||||
args.Handled = true;
|
||||
};
|
||||
|
||||
sub2.Activating += (_, args) =>
|
||||
{
|
||||
commandLogList.Add ($"{args.Context!.Source!.Id}:{args.Context!.Command}");
|
||||
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
|
||||
commandLog.MoveEnd ();
|
||||
args.Handled = true;
|
||||
};
|
||||
|
||||
sub2.Accepting += (_, args) =>
|
||||
{
|
||||
commandLogList.Add ($"{args.Context!.Source!.Id}:{args.Context!.Command}");
|
||||
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
|
||||
commandLog.MoveEnd ();
|
||||
args.Handled = true;
|
||||
};
|
||||
|
||||
@@ -265,10 +265,12 @@ public sealed class UICatalogRunnable : Runnable
|
||||
|
||||
_diagnosticFlagsSelector.Activating += (_, args) =>
|
||||
{
|
||||
_diagnosticFlags =
|
||||
(ViewDiagnosticFlags)(int)args.Context!.Source!
|
||||
.Data!; // (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value;
|
||||
Diagnostics = _diagnosticFlags;
|
||||
if (args.Context?.TryGetSource (out View? sourceView) == true)
|
||||
{
|
||||
_diagnosticFlags =
|
||||
(ViewDiagnosticFlags)(int)sourceView.Data!; // (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value;
|
||||
Diagnostics = _diagnosticFlags;
|
||||
}
|
||||
};
|
||||
|
||||
var diagFlagMenuItem = new MenuItem { CommandView = _diagnosticFlagsSelector, HelpText = "View Diagnostics" };
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
/// </code>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="View.InvokeCommand"/>.
|
||||
/// <seealso cref="View.InvokeCommand"/>
|
||||
/// .
|
||||
#pragma warning restore CS1574, CS0419 // XML comment has cref attribute that could not be resolved
|
||||
public record struct CommandContext : ICommandContext
|
||||
{
|
||||
@@ -22,21 +23,24 @@ public record struct CommandContext : ICommandContext
|
||||
/// Initializes a new instance with the specified <see cref="Command"/>.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being invoked.</param>
|
||||
/// <param name="source">The view that is the source of the command invocation.</param>
|
||||
/// <param name="source">A weak reference to the view that is the source of the command invocation.</param>
|
||||
/// <param name="binding">The binding that triggered the command, if any.</param>
|
||||
public CommandContext (Command command, View? source, IInputBinding? binding)
|
||||
public CommandContext (Command command, WeakReference<View>? source, IInputBinding? binding)
|
||||
{
|
||||
Command = command;
|
||||
Binding = binding;
|
||||
Source = source;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <inheritdoc/>
|
||||
public Command Command { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public View? Source { get; set; }
|
||||
/// <inheritdoc/>
|
||||
public WeakReference<View>? Source { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <inheritdoc/>
|
||||
public IInputBinding? Binding { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString () => $"{Command} (Source={Source.ToIdentifyingString ()}, Binding={Binding})";
|
||||
}
|
||||
|
||||
44
Terminal.Gui/Input/CommandContextExtensions.cs
Normal file
44
Terminal.Gui/Input/CommandContextExtensions.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
namespace Terminal.Gui.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="ICommandContext"/>.
|
||||
/// </summary>
|
||||
public static class CommandContextExtensions
|
||||
{
|
||||
/// <param name="context">The command context.</param>
|
||||
extension (ICommandContext? context)
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get the source <see cref="View"/> from a command context.
|
||||
/// </summary>
|
||||
/// <param name="source">
|
||||
/// When this method returns, contains the source View if the context is not null and the source weak reference
|
||||
/// target is still alive; otherwise, null.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the context is not null and the source weak reference target is still alive;
|
||||
/// otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is a convenience method to simplify accessing the source view from a command context.
|
||||
/// It combines null-checking the context and retrieving the weak reference target in one call.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Example usage:
|
||||
/// <code>
|
||||
/// if (commandContext.TryGetSource(out View? view))
|
||||
/// {
|
||||
/// // use view
|
||||
/// }
|
||||
/// </code>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool TryGetSource (out View? source)
|
||||
{
|
||||
source = null;
|
||||
|
||||
return context?.Source?.TryGetTarget (out source) == true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
|
||||
/// <summary>
|
||||
/// Describes the context in which a <see cref="Command"/> is being invoked. <see cref="CommandContext{TBindingType}"/>
|
||||
/// inherits from this interface.
|
||||
/// Describes the context in which a <see cref="Command"/> is being invoked.
|
||||
/// When a <see cref="Command"/> is invoked,
|
||||
/// a context object is passed to Command handlers as an <see cref="ICommandContext"/> reference.
|
||||
/// </summary>
|
||||
@@ -18,10 +17,14 @@ public interface ICommandContext
|
||||
public Command Command { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The View that was the source of the command invocation, if any.
|
||||
/// A weak reference to the View that was the source of the command invocation, if any.
|
||||
/// (e.g. the view the user clicked on or the view that had focus when a key was pressed).
|
||||
/// Use <c>Source?.TryGetTarget(out View? view)</c> to safely access the source view.
|
||||
/// </summary>
|
||||
public View? Source { get; set; }
|
||||
/// <remarks>
|
||||
/// Uses WeakReference to prevent memory leaks when views are disposed during command propagation.
|
||||
/// </remarks>
|
||||
public WeakReference<View>? Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The binding that triggered the command.
|
||||
|
||||
@@ -473,7 +473,7 @@ public partial class View // Command APIs
|
||||
_commandImplementations.TryGetValue (Command.NotBound, out implementation);
|
||||
}
|
||||
|
||||
return implementation! (new CommandContext { Command = command, Source = this, Binding = binding });
|
||||
return implementation! (new CommandContext { Command = command, Source = new WeakReference<View> (this), Binding = binding });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -516,6 +516,6 @@ public partial class View // Command APIs
|
||||
_commandImplementations.TryGetValue (Command.NotBound, out implementation);
|
||||
}
|
||||
|
||||
return implementation! (new CommandContext { Command = command, Source = this, Binding = null });
|
||||
return implementation! (new CommandContext { Command = command, Source = new WeakReference<View> (this), Binding = null });
|
||||
}
|
||||
}
|
||||
|
||||
40
Terminal.Gui/ViewBase/ViewExtensions.cs
Normal file
40
Terminal.Gui/ViewBase/ViewExtensions.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
namespace Terminal.Gui.ViewBase;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="View"/> to support debugging and logging.
|
||||
/// </summary>
|
||||
public static class ViewExtensions
|
||||
{
|
||||
/// <param name="view">The view to identify.</param>
|
||||
extension (View? view)
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a formatted string that identifies the View for debugging/logging purposes.
|
||||
/// </summary>
|
||||
/// <returns>A string identifying the View using Id, Title, Text, or type name.</returns>
|
||||
public string ToIdentifyingString ()
|
||||
{
|
||||
if (view is null)
|
||||
{
|
||||
return "(null)";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty (view.Id))
|
||||
{
|
||||
return $"{view.Id}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty (view.Title))
|
||||
{
|
||||
return $"(\"{view.Title}\")";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty (view.Text))
|
||||
{
|
||||
return $"(\"{view.Text}\")";
|
||||
}
|
||||
|
||||
return view.GetType ().Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Terminal.Gui/ViewBase/WeakReferenceExtensions.cs
Normal file
58
Terminal.Gui/ViewBase/WeakReferenceExtensions.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace Terminal.Gui.ViewBase;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="WeakReference{T}"/> when T is <see cref="View"/>.
|
||||
/// </summary>
|
||||
public static class WeakReferenceExtensions
|
||||
{
|
||||
/// <param name="weakRef">The weak reference to format.</param>
|
||||
extension (WeakReference<View>? weakRef)
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a formatted string representation of the <see cref="WeakReference{T}"/> to a View.
|
||||
/// </summary>
|
||||
/// <returns>A string identifying the referenced View, or "(null)" if the reference is null or dead.</returns>
|
||||
public string ToIdentifyingString ()
|
||||
{
|
||||
if (weakRef is null || !weakRef.TryGetTarget (out View? view))
|
||||
{
|
||||
return "(null)";
|
||||
}
|
||||
|
||||
return view.ToIdentifyingString ();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the source <see cref="View"/> from a <see cref="WeakReference{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="source">
|
||||
/// When this method returns, contains the target View if the weak reference is not null and the target is still alive;
|
||||
/// otherwise, null.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the weak reference is not null and the target is still alive;
|
||||
/// otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is a convenience method to simplify the common pattern of checking a weak reference
|
||||
/// and retrieving its target. It's particularly useful with <see cref="ICommandContext.Source"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Example usage:
|
||||
/// <code>
|
||||
/// if (commandContext.Source.TryGetSource(out View? view))
|
||||
/// {
|
||||
/// // use view
|
||||
/// }
|
||||
/// </code>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool TryGetSource (out View? source)
|
||||
{
|
||||
source = null;
|
||||
|
||||
return weakRef?.TryGetTarget (out source) == true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public class ComboBox : View, IDesignable
|
||||
AddCommand (Command.Accept,
|
||||
ctx =>
|
||||
{
|
||||
if (ctx?.Source == _search)
|
||||
if (ctx?.TryGetSource (out View? sourceView) == true && sourceView == _search)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -851,7 +851,7 @@ public class ComboBox : View, IDesignable
|
||||
}
|
||||
else if (isMousePositionValid)
|
||||
{
|
||||
return RaiseAccepting (new CommandContext (Command.Accept, this, new InputBinding ())) == true;
|
||||
return RaiseAccepting (new CommandContext (Command.Accept, new WeakReference<View> (this), new InputBinding ())) == true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -116,13 +116,13 @@ public class Dialog : Dialog<int>
|
||||
/// <returns></returns>
|
||||
protected override bool OnAccepting (CommandEventArgs args)
|
||||
{
|
||||
if (!Buttons.Contains (args.Context?.Source))
|
||||
if (!args.Context.TryGetSource (out View? sourceView) || !Buttons.Contains (sourceView))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int buttonIndex = Buttons.IndexOf (args.Context?.Source);
|
||||
Result = buttonIndex != -1 ? buttonIndex : Buttons.IndexOf (Buttons.FirstOrDefault (b => b.IsDefault));
|
||||
int buttonIndex = Array.IndexOf (Buttons, sourceView);
|
||||
Result = buttonIndex != -1 ? buttonIndex : Array.IndexOf (Buttons, Buttons.FirstOrDefault (b => b.IsDefault));
|
||||
|
||||
RequestStop ();
|
||||
return true;
|
||||
|
||||
@@ -190,12 +190,12 @@ public class Dialog<TResult> : Runnable<TResult>, IDesignable
|
||||
/// <returns></returns>
|
||||
protected override bool OnAccepting (CommandEventArgs args)
|
||||
{
|
||||
if (!Buttons.Contains (args.Context?.Source))
|
||||
if (!args.Context.TryGetSource (out View? sourceView) || !Buttons.Contains (sourceView))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Buttons.FirstOrDefault (v => v is Button { IsDefault: true }) == args.Context?.Source)
|
||||
if (Buttons.FirstOrDefault (v => v is Button { IsDefault: true }) == sourceView)
|
||||
{
|
||||
// Default button pressed
|
||||
return false;
|
||||
|
||||
@@ -451,10 +451,10 @@ public class MenuBar : Menu, IDesignable
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAccepted (CommandEventArgs args)
|
||||
{
|
||||
// Logging.Debug ($"{Title} ({args.Context?.Source?.Title}) Command: {args.Context?.Command}");
|
||||
// Logging.Debug ($"{Title} ({args.Context?.Source.ToIdentifyingString ()}) Command: {args.Context?.Command}");
|
||||
base.OnAccepted (args);
|
||||
|
||||
if (SubViews.OfType<MenuBarItem> ().Contains (args.Context?.Source))
|
||||
if (args.Context.TryGetSource (out View? sourceView) && SubViews.OfType<MenuBarItem> ().Contains (sourceView))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -465,10 +465,10 @@ public class MenuBar : Menu, IDesignable
|
||||
/// <inheritdoc/>
|
||||
protected override bool OnAccepting (CommandEventArgs args)
|
||||
{
|
||||
// Logging.Debug ($"{Title} ({args.Context?.Source?.Title})");
|
||||
// Logging.Debug ($"{Title} ({args.Context?.Source.ToIdentifyingString ()})");
|
||||
|
||||
// TODO: Ensure sourceMenuBar is actually one of our bar items
|
||||
if (Visible && Enabled && args.Context?.Source is MenuBarItem { PopoverMenuOpen: false } sourceMenuBarItem)
|
||||
if (Visible && Enabled && args.Context.TryGetSource (out View? sourceView) && sourceView is MenuBarItem { PopoverMenuOpen: false } sourceMenuBarItem)
|
||||
{
|
||||
if (!CanFocus)
|
||||
{
|
||||
|
||||
@@ -619,13 +619,13 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
|
||||
|
||||
private void MenuAccepted (object? sender, CommandEventArgs e)
|
||||
{
|
||||
// Logging.Debug ($"{Title} ({e.Context?.Source?.Title}) Command: {e.Context?.Command}");
|
||||
// Logging.Debug ($"{Title} ({e.Context?.Source.ToIdentifyingString ()}) Command: {e.Context?.Command}");
|
||||
|
||||
if (e.Context?.Source is MenuItem { SubMenu: null })
|
||||
if (e.Context.TryGetSource (out View? sourceView) && sourceView is MenuItem { SubMenu: null })
|
||||
{
|
||||
HideAndRemoveSubMenu (_root);
|
||||
}
|
||||
else if (e.Context?.Source is MenuItem { SubMenu: { } } menuItemWithSubMenu)
|
||||
else if (e.Context.TryGetSource (out View? sourceView2) && sourceView2 is MenuItem { SubMenu: { } } menuItemWithSubMenu)
|
||||
{
|
||||
ShowSubMenu (menuItemWithSubMenu);
|
||||
}
|
||||
@@ -668,7 +668,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
|
||||
}
|
||||
|
||||
// Only raise Accepted if the command came from one of our MenuItems
|
||||
if (GetMenuItemsOfAllSubMenus ().Contains (args.Context?.Source))
|
||||
if (args.Context.TryGetSource (out View? sourceView) && GetMenuItemsOfAllSubMenus ().Contains (sourceView))
|
||||
{
|
||||
// Logging.Debug ($"{Title} - Calling RaiseAccepted {args.Context?.Command}");
|
||||
RaiseAccepted (args.Context);
|
||||
|
||||
@@ -239,7 +239,7 @@ public class ScrollSlider : View, IOrientation, IDesignable
|
||||
OnScrolled (distance);
|
||||
Scrolled?.Invoke (this, new (in distance));
|
||||
|
||||
RaiseActivating (new CommandContext (Command.Activate, this, new KeyBinding ([Command.Activate], null, distance)));
|
||||
RaiseActivating (new CommandContext (Command.Activate, new WeakReference<View> (this), new KeyBinding ([Command.Activate], null, distance)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -70,7 +70,7 @@ public class OptionSelector : SelectorBase, IDesignable
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!CanFocus || args.Context?.Source is not CheckBox checkBox)
|
||||
if (!CanFocus || !args.Context.TryGetSource (out View? source) || source is not CheckBox checkBox)
|
||||
{
|
||||
Cycle ();
|
||||
|
||||
|
||||
@@ -258,7 +258,9 @@ public class Shortcut : View, IOrientation, IDesignable
|
||||
{
|
||||
KeyBinding? keyBinding = commandContext?.Binding as KeyBinding?;
|
||||
|
||||
Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) Command: {commandContext?.Command}");
|
||||
string sourceTitle = commandContext?.TryGetSource (out View? sourceView) == true ? sourceView.Title : "(null)";
|
||||
|
||||
Logging.Debug ($"{Title} ({sourceTitle}) Command: {commandContext?.Command}");
|
||||
|
||||
if (keyBinding is { } kb && kb.Data != this)
|
||||
{
|
||||
@@ -267,12 +269,12 @@ public class Shortcut : View, IOrientation, IDesignable
|
||||
// If this causes CommandView to raise Accept, we eat it
|
||||
KeyBinding updatedBinding = kb with { Data = this };
|
||||
|
||||
Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - Invoking Activate on CommandView ({CommandView.GetType ().Name}).");
|
||||
Logging.Debug ($"{Title} ({sourceTitle}) - Invoking Activate on CommandView ({CommandView.GetType ().Name}).");
|
||||
|
||||
CommandView.InvokeCommand (Command.Activate, updatedBinding);
|
||||
}
|
||||
|
||||
Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - RaiseActivating ...");
|
||||
Logging.Debug ($"{Title} ({sourceTitle}) - RaiseActivating ...");
|
||||
|
||||
if (RaiseActivating (commandContext) is true)
|
||||
{
|
||||
@@ -282,7 +284,7 @@ public class Shortcut : View, IOrientation, IDesignable
|
||||
if (CanFocus && SuperView is { CanFocus: true })
|
||||
{
|
||||
// The default HotKey handler sets Focus
|
||||
Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - SetFocus...");
|
||||
Logging.Debug ($"{Title} ({sourceTitle}) - SetFocus...");
|
||||
SetFocus ();
|
||||
}
|
||||
|
||||
@@ -290,10 +292,10 @@ public class Shortcut : View, IOrientation, IDesignable
|
||||
|
||||
if (commandContext is { Source: null })
|
||||
{
|
||||
commandContext.Source = this;
|
||||
commandContext.Source = new WeakReference<View> (this);
|
||||
}
|
||||
|
||||
Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - Calling RaiseAccepting...");
|
||||
Logging.Debug ($"{Title} ({sourceTitle}) - Calling RaiseAccepting...");
|
||||
cancel = RaiseAccepting (commandContext) is true;
|
||||
|
||||
if (cancel)
|
||||
@@ -303,7 +305,7 @@ public class Shortcut : View, IOrientation, IDesignable
|
||||
|
||||
if (Action is { })
|
||||
{
|
||||
Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - Invoke Action...");
|
||||
Logging.Debug ($"{Title} ({sourceTitle}) - Invoke Action...");
|
||||
Action.Invoke ();
|
||||
|
||||
// Assume if there's a subscriber to Action, it's handled.
|
||||
|
||||
@@ -532,6 +532,7 @@
|
||||
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=Button_0020button/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=can_0020opt_002Din/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=ESC_0020ESC/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=Id/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=Label_0020label/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=Mouse_0020mouse/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=_005B/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@@ -16,10 +16,12 @@ public class CommandContextTests
|
||||
View sourceView = new () { Id = "sourceView" };
|
||||
KeyBinding keyBinding = new ([Command.Activate]) { Key = Key.Enter };
|
||||
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = sourceView, Binding = keyBinding };
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = new WeakReference<View>(sourceView), Binding = keyBinding };
|
||||
|
||||
Assert.Equal (Command.Activate, ctx.Command);
|
||||
Assert.Equal (sourceView, ctx.Source);
|
||||
Assert.NotNull (ctx.Source);
|
||||
Assert.True (ctx.Source.TryGetTarget (out View? view));
|
||||
Assert.Equal (sourceView, view);
|
||||
Assert.NotNull (ctx.Binding);
|
||||
|
||||
if (ctx.Binding is KeyBinding kb)
|
||||
@@ -38,10 +40,12 @@ public class CommandContextTests
|
||||
View sourceView = new () { Id = "sourceView" };
|
||||
MouseBinding mouseBinding = new ([Command.Activate], MouseFlags.LeftButtonClicked);
|
||||
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = sourceView, Binding = mouseBinding };
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = new WeakReference<View>(sourceView), Binding = mouseBinding };
|
||||
|
||||
Assert.Equal (Command.Activate, ctx.Command);
|
||||
Assert.Equal (sourceView, ctx.Source);
|
||||
Assert.NotNull (ctx.Source);
|
||||
Assert.True (ctx.Source.TryGetTarget (out View? view));
|
||||
Assert.Equal (sourceView, view);
|
||||
Assert.NotNull (ctx.Binding);
|
||||
|
||||
if (ctx.Binding is MouseBinding mb)
|
||||
@@ -62,7 +66,7 @@ public class CommandContextTests
|
||||
[Fact]
|
||||
public void CommandContext_ImplementsICommandContext ()
|
||||
{
|
||||
CommandContext ctx = new () { Command = Command.Accept, Source = new View () };
|
||||
CommandContext ctx = new () { Command = Command.Accept, Source = new WeakReference<View>(new View ()) };
|
||||
|
||||
ICommandContext iCtx = ctx;
|
||||
|
||||
@@ -76,12 +80,14 @@ public class CommandContextTests
|
||||
View originalSource = new () { Id = "original" };
|
||||
View newSource = new () { Id = "new" };
|
||||
|
||||
CommandContext ctx = new () { Command = Command.Accept, Source = originalSource };
|
||||
CommandContext ctx = new () { Command = Command.Accept, Source = new WeakReference<View>(originalSource) };
|
||||
|
||||
ICommandContext iCtx = ctx;
|
||||
iCtx.Source = newSource;
|
||||
iCtx.Source = new WeakReference<View>(newSource);
|
||||
|
||||
Assert.Equal (newSource, iCtx.Source);
|
||||
Assert.NotNull (iCtx.Source);
|
||||
Assert.True (iCtx.Source.TryGetTarget (out View? view));
|
||||
Assert.Equal (newSource, view);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -94,7 +100,7 @@ public class CommandContextTests
|
||||
ICommandContext ctx = new CommandContext
|
||||
{
|
||||
Command = Command.Activate,
|
||||
Source = new View (),
|
||||
Source = new WeakReference<View>(new View ()),
|
||||
Binding = new KeyBinding ([Command.Activate]) { Key = Key.Enter }
|
||||
};
|
||||
|
||||
@@ -115,7 +121,7 @@ public class CommandContextTests
|
||||
MouseBinding mouseBinding = new ([Command.Activate], MouseFlags.LeftButtonClicked) { Source = new View { Id = "mouseSource" } };
|
||||
mouseBinding.MouseEvent = new Mouse { Flags = MouseFlags.LeftButtonClicked, Position = new Point (10, 20) };
|
||||
|
||||
ICommandContext ctx = new CommandContext { Command = Command.Activate, Source = new View (), Binding = mouseBinding };
|
||||
ICommandContext ctx = new CommandContext { Command = Command.Activate, Source = new WeakReference<View>(new View ()), Binding = mouseBinding };
|
||||
|
||||
// This is the actual pattern used in production code
|
||||
if (ctx.Binding is MouseBinding { MouseEvent: { } mouse })
|
||||
@@ -137,7 +143,7 @@ public class CommandContextTests
|
||||
MouseEvent = null // Explicitly set to null
|
||||
};
|
||||
|
||||
ICommandContext ctx = new CommandContext { Command = Command.Activate, Source = new View (), Binding = mouseBinding };
|
||||
ICommandContext ctx = new CommandContext { Command = Command.Activate, Source = new WeakReference<View>(new View ()), Binding = mouseBinding };
|
||||
|
||||
// Pattern should NOT match when MouseEvent is null
|
||||
bool matched = ctx.Binding is MouseBinding { MouseEvent: { } };
|
||||
@@ -151,7 +157,7 @@ public class CommandContextTests
|
||||
ICommandContext ctx = new CommandContext
|
||||
{
|
||||
Command = Command.Activate,
|
||||
Source = new View (),
|
||||
Source = new WeakReference<View>(new View ()),
|
||||
Binding = new KeyBinding ([Command.Activate])
|
||||
};
|
||||
|
||||
@@ -173,10 +179,12 @@ public class CommandContextTests
|
||||
|
||||
KeyBinding keyBinding = new ([Command.Activate]) { Key = Key.A, Source = bindingSource };
|
||||
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = contextSource, Binding = keyBinding };
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = new WeakReference<View>(contextSource), Binding = keyBinding };
|
||||
|
||||
// Both sources are accessible
|
||||
Assert.Equal ("contextSource", ctx.Source?.Id);
|
||||
Assert.NotNull (ctx.Source);
|
||||
Assert.True (ctx.Source.TryGetTarget (out View? ctxView));
|
||||
Assert.Equal ("contextSource", ctxView!.Id);
|
||||
|
||||
if (ctx.Binding is KeyBinding kb)
|
||||
{
|
||||
@@ -196,10 +204,12 @@ public class CommandContextTests
|
||||
|
||||
MouseBinding mouseBinding = new ([Command.Activate], MouseFlags.LeftButtonClicked) { Source = bindingSource };
|
||||
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = contextSource, Binding = mouseBinding };
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = new WeakReference<View>(contextSource), Binding = mouseBinding };
|
||||
|
||||
// Both sources are accessible
|
||||
Assert.Equal ("contextSource", ctx.Source?.Id);
|
||||
Assert.NotNull (ctx.Source);
|
||||
Assert.True (ctx.Source.TryGetTarget (out View? ctxView));
|
||||
Assert.Equal ("contextSource", ctxView!.Id);
|
||||
|
||||
if (ctx.Binding is MouseBinding mb)
|
||||
{
|
||||
@@ -220,7 +230,7 @@ public class CommandContextTests
|
||||
{
|
||||
KeyBinding keyBinding = new ([Command.Accept]) { Key = Key.Enter, Source = new View { Id = "keySource" } };
|
||||
|
||||
CommandContext ctx = new () { Command = Command.Accept, Source = new View { Id = "invoker" }, Binding = keyBinding };
|
||||
CommandContext ctx = new () { Command = Command.Accept, Source = new WeakReference<View>(new View { Id = "invoker" }), Binding = keyBinding };
|
||||
|
||||
CommandEventArgs args = new () { Context = ctx };
|
||||
|
||||
@@ -242,7 +252,7 @@ public class CommandContextTests
|
||||
{
|
||||
MouseBinding mouseBinding = new ([Command.Activate], MouseFlags.RightButtonClicked) { Source = new View { Id = "mouseSource" } };
|
||||
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = new View { Id = "invoker" }, Binding = mouseBinding };
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = new WeakReference<View>(new View { Id = "invoker" }), Binding = mouseBinding };
|
||||
|
||||
CommandEventArgs args = new () { Context = ctx };
|
||||
|
||||
@@ -341,4 +351,85 @@ public class CommandContextTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryGetSource Extension Method Tests
|
||||
|
||||
[Fact]
|
||||
public void TryGetSource_WithValidWeakReference_ReturnsTrue ()
|
||||
{
|
||||
View sourceView = new () { Id = "testView" };
|
||||
WeakReference<View> weakRef = new (sourceView);
|
||||
|
||||
bool result = weakRef.TryGetSource (out View? retrievedView);
|
||||
|
||||
Assert.True (result);
|
||||
Assert.NotNull (retrievedView);
|
||||
Assert.Equal (sourceView, retrievedView);
|
||||
Assert.Equal ("testView", retrievedView!.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetSource_WithNullWeakReference_ReturnsFalse ()
|
||||
{
|
||||
WeakReference<View>? weakRef = null;
|
||||
|
||||
bool result = weakRef.TryGetSource (out View? retrievedView);
|
||||
|
||||
Assert.False (result);
|
||||
Assert.Null (retrievedView);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandContext_TryGetSource_WithValidSource_ReturnsTrue ()
|
||||
{
|
||||
View sourceView = new () { Id = "contextSource" };
|
||||
CommandContext ctx = new () { Command = Command.Accept, Source = new WeakReference<View> (sourceView) };
|
||||
|
||||
bool result = ctx.TryGetSource (out View? retrievedView);
|
||||
|
||||
Assert.True (result);
|
||||
Assert.NotNull (retrievedView);
|
||||
Assert.Equal (sourceView, retrievedView);
|
||||
Assert.Equal ("contextSource", retrievedView!.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandContext_TryGetSource_WithNullContext_ReturnsFalse ()
|
||||
{
|
||||
ICommandContext? ctx = null;
|
||||
|
||||
bool result = ctx.TryGetSource (out View? retrievedView);
|
||||
|
||||
Assert.False (result);
|
||||
Assert.Null (retrievedView);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandContext_TryGetSource_WithNullSource_ReturnsFalse ()
|
||||
{
|
||||
CommandContext ctx = new () { Command = Command.Accept, Source = null };
|
||||
|
||||
bool result = ctx.TryGetSource (out View? retrievedView);
|
||||
|
||||
Assert.False (result);
|
||||
Assert.Null (retrievedView);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandContext_TryGetSource_CanBeUsedInPatternMatching ()
|
||||
{
|
||||
View sourceView = new Button { Id = "testButton" };
|
||||
CommandContext ctx = new () { Command = Command.Accept, Source = new WeakReference<View> (sourceView) };
|
||||
|
||||
if (ctx.TryGetSource (out View? view) && view is Button button)
|
||||
{
|
||||
Assert.Equal ("testButton", button.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail ("Should have retrieved Button from context");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -166,10 +166,12 @@ public class InputBindingTests
|
||||
View source = new () { Id = "contextSource" };
|
||||
InputBinding binding = new ([Command.Activate], source, "contextData");
|
||||
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = source, Binding = binding };
|
||||
CommandContext ctx = new () { Command = Command.Activate, Source = new WeakReference<View>(source), Binding = binding };
|
||||
|
||||
Assert.Equal (Command.Activate, ctx.Command);
|
||||
Assert.Equal (source, ctx.Source);
|
||||
Assert.NotNull (ctx.Source);
|
||||
Assert.True (ctx.Source.TryGetTarget (out View? view));
|
||||
Assert.Equal (source, view);
|
||||
Assert.NotNull (ctx.Binding);
|
||||
|
||||
if (ctx.Binding is InputBinding ib)
|
||||
|
||||
Reference in New Issue
Block a user