Add step actions
This commit is contained in:
parent
c431f43a30
commit
1bb448b1a6
118
API_Reference.md
118
API_Reference.md
@ -203,10 +203,48 @@ Dismisses the intro view and continues with the tour.
|
||||
| `TourCompleted` | `EventArgs` | Raised when the user completes all steps. |
|
||||
| `TourSkipped` | `EventArgs` | Raised when the user skips the tour. |
|
||||
| `TourEnded` | `EventArgs` | Raised when the tour ends (any reason). |
|
||||
| `StepEntering` | `StepActionEventArgs` | Raised before entering a step. Async. Can set `Skip = true`. |
|
||||
| `StepEntered` | `StepActionEventArgs` | Raised after step is fully visible (after animations). |
|
||||
| `StepLeaving` | `StepActionEventArgs` | Raised before leaving the current step. Async. |
|
||||
| `StepChanged` | `OnboardingStepEventArgs` | Raised when moving to a new step. |
|
||||
| `IntroShown` | `EventArgs` | Raised when the intro view is shown. |
|
||||
| `IntroDismissed` | `EventArgs` | Raised when the intro view is dismissed. |
|
||||
|
||||
### Step Action Registration
|
||||
|
||||
```csharp
|
||||
void RegisterStepEnteringAction(string stepKey, Func<OnboardingStep, CancellationToken, Task> action)
|
||||
```
|
||||
Registers an action to execute when entering a specific step (by StepKey). The action runs before the spotlight animates to the step.
|
||||
|
||||
---
|
||||
|
||||
```csharp
|
||||
void RegisterStepLeavingAction(string stepKey, Func<OnboardingStep, CancellationToken, Task> action)
|
||||
```
|
||||
Registers an action to execute when leaving a specific step (by StepKey). The action runs before navigating away.
|
||||
|
||||
---
|
||||
|
||||
```csharp
|
||||
bool UnregisterStepEnteringAction(string stepKey)
|
||||
```
|
||||
Removes a registered entering action for a step. Returns true if the action was removed.
|
||||
|
||||
---
|
||||
|
||||
```csharp
|
||||
bool UnregisterStepLeavingAction(string stepKey)
|
||||
```
|
||||
Removes a registered leaving action for a step. Returns true if the action was removed.
|
||||
|
||||
---
|
||||
|
||||
```csharp
|
||||
void ClearStepActions()
|
||||
```
|
||||
Clears all registered step actions (both entering and leaving).
|
||||
|
||||
### Usage Example
|
||||
|
||||
```xml
|
||||
@ -268,6 +306,13 @@ Static class providing attached properties for tagging UI elements as onboarding
|
||||
| `Placement` | `CalloutPlacement` | `Auto` | Where to place the callout relative to this element. |
|
||||
| `TapBehavior` | `SpotlightTapBehavior` | `None` | Behavior when user taps the spotlight. |
|
||||
|
||||
#### Step Actions
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `OnEntering` | `Func<OnboardingStep, CancellationToken, Task>?` | `null` | Action to execute when entering this step. Runs before spotlight animation. |
|
||||
| `OnLeaving` | `Func<OnboardingStep, CancellationToken, Task>?` | `null` | Action to execute when leaving this step. Runs before navigating away. |
|
||||
|
||||
### Static Methods
|
||||
|
||||
```csharp
|
||||
@ -312,6 +357,19 @@ Onboarding.SetSpotlightShape(myButton, SpotlightShape.Circle);
|
||||
// Read properties
|
||||
var key = Onboarding.GetStepKey(myButton);
|
||||
var title = Onboarding.GetTitle(myButton);
|
||||
|
||||
// Set step actions (async actions that run when entering/leaving this step)
|
||||
Onboarding.SetOnEntering(myButton, async (step, cancellationToken) =>
|
||||
{
|
||||
// Prepare UI before spotlight appears (e.g., switch tabs, open drawer)
|
||||
MainTabView.SelectedIndex = 2;
|
||||
await Task.Delay(100);
|
||||
});
|
||||
|
||||
Onboarding.SetOnLeaving(myButton, async (step, cancellationToken) =>
|
||||
{
|
||||
// Cleanup when leaving this step
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
@ -336,6 +394,8 @@ Represents a single step in an onboarding tour. Created automatically from tagge
|
||||
| `SpotlightPadding` | `Thickness` | Padding around target. Default: `8`. |
|
||||
| `SpotlightCornerRadius` | `double` | Corner radius for rounded rectangle. Default: `8.0`. |
|
||||
| `TapBehavior` | `SpotlightTapBehavior` | Behavior when tapping spotlight. Default: `None`. |
|
||||
| `OnEntering` | `Func<OnboardingStep, CancellationToken, Task>?` | Action to execute when entering this step. |
|
||||
| `OnLeaving` | `Func<OnboardingStep, CancellationToken, Task>?` | Action to execute when leaving this step. |
|
||||
|
||||
### Static Methods
|
||||
|
||||
@ -559,6 +619,64 @@ TourHost.StepChanged += (sender, e) =>
|
||||
|
||||
---
|
||||
|
||||
### StepActionEventArgs
|
||||
|
||||
Event arguments for step action events (`StepEntering`, `StepEntered`, `StepLeaving`).
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Step` | `OnboardingStep` | The step being entered or left. |
|
||||
| `StepIndex` | `int` | Zero-based index of the step. |
|
||||
| `TotalSteps` | `int` | Total number of steps in the tour. |
|
||||
| `PreviousStepIndex` | `int` | Index of the previous step (-1 if first step). |
|
||||
| `IsForward` | `bool` | Whether navigation is moving forward (true) or backward (false). |
|
||||
| `Skip` | `bool` | Set to `true` to skip this step (only applies to `StepEntering`). |
|
||||
| `CancellationToken` | `CancellationToken` | Cancellation token for the current operation. |
|
||||
|
||||
### Step Action Usage Example
|
||||
|
||||
```csharp
|
||||
// Register action by StepKey
|
||||
TourHost.RegisterStepEnteringAction("settings-tab", async (step, token) =>
|
||||
{
|
||||
// Switch to settings tab before spotlight appears
|
||||
SettingsTabView.SelectedIndex = 1;
|
||||
await Task.Delay(150); // Wait for tab transition
|
||||
});
|
||||
|
||||
// Use event for conditional skipping
|
||||
TourHost.StepEntering += async (sender, e) =>
|
||||
{
|
||||
// Skip premium-only steps for free users
|
||||
if (e.Step.StepKey?.StartsWith("premium-") == true && !User.IsPremium)
|
||||
{
|
||||
e.Skip = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Track when steps are viewed
|
||||
TourHost.StepEntered += (sender, e) =>
|
||||
{
|
||||
Analytics.Track("tour_step_viewed", new {
|
||||
step_key = e.Step.StepKey,
|
||||
step_index = e.StepIndex,
|
||||
is_forward = e.IsForward
|
||||
});
|
||||
};
|
||||
|
||||
// Cleanup when leaving steps
|
||||
TourHost.StepLeaving += async (sender, e) =>
|
||||
{
|
||||
if (e.Step.StepKey == "drawer-step")
|
||||
{
|
||||
Shell.Current.FlyoutIsPresented = false;
|
||||
await Task.Delay(200);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Integration Example
|
||||
|
||||
### XAML (MainPage.xaml)
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
<PackageId>MarketAlly.SpotlightTour.Maui</PackageId>
|
||||
<Title>MASpotlightTour - Feature Tour & Onboarding for .NET MAUI</Title>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>1.1.0</Version>
|
||||
<Authors>David H Friedel Jr</Authors>
|
||||
<Company>MarketAlly</Company>
|
||||
<Description>A powerful, declarative spotlight tour and onboarding library for .NET MAUI applications. Create beautiful feature tours with spotlight overlays, callout cards, and step-by-step guidance using simple XAML attached properties. Features include: multiple display modes (spotlight with callout, callout-only, inline labels), flexible callout positioning (following, fixed corner, auto-corner), customizable spotlight shapes (rectangle, rounded rectangle, circle), corner navigator with step indicators, intro/welcome views, auto-advance timers, tour looping, light/dark theme support, smooth animations, and awaitable tour completion for action chaining. Perfect for user onboarding, feature discovery, and interactive tutorials. Supports iOS, Android, Windows, and macOS.</Description>
|
||||
@ -30,6 +30,15 @@
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>
|
||||
Version 1.1.0 - Step Actions:
|
||||
- NEW: Step Actions - Execute custom async code when entering/leaving tour steps
|
||||
- NEW: OnEntering/OnLeaving attached properties for per-element actions
|
||||
- NEW: RegisterStepEnteringAction/RegisterStepLeavingAction for centralized registration by StepKey
|
||||
- NEW: StepEntering event with Skip property for conditional step skipping
|
||||
- NEW: StepEntered event fired after step animations complete
|
||||
- NEW: StepLeaving event for cleanup before navigation
|
||||
- Perfect for UI preparation: switching tabs, opening drawers, triggering animations
|
||||
|
||||
Version 1.0.0 - Initial Release:
|
||||
- Declarative tour steps via XAML attached properties
|
||||
- Three display modes: SpotlightWithCallout, CalloutOnly, SpotlightWithInlineLabel
|
||||
|
||||
@ -225,6 +225,49 @@ public static class Onboarding
|
||||
|
||||
#endregion
|
||||
|
||||
#region OnEntering
|
||||
|
||||
/// <summary>
|
||||
/// Action to execute when the tour enters this step.
|
||||
/// The action runs before the spotlight animates to the element, allowing UI preparation
|
||||
/// (e.g., switching tabs, opening drawers) to complete first.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty OnEnteringProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"OnEntering",
|
||||
typeof(Func<OnboardingStep, CancellationToken, Task>),
|
||||
typeof(Onboarding),
|
||||
defaultValue: null);
|
||||
|
||||
public static Func<OnboardingStep, CancellationToken, Task>? GetOnEntering(BindableObject view) =>
|
||||
(Func<OnboardingStep, CancellationToken, Task>?)view.GetValue(OnEnteringProperty);
|
||||
|
||||
public static void SetOnEntering(BindableObject view, Func<OnboardingStep, CancellationToken, Task>? value) =>
|
||||
view.SetValue(OnEnteringProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region OnLeaving
|
||||
|
||||
/// <summary>
|
||||
/// Action to execute when the tour leaves this step.
|
||||
/// The action runs before navigating to the next/previous step.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty OnLeavingProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"OnLeaving",
|
||||
typeof(Func<OnboardingStep, CancellationToken, Task>),
|
||||
typeof(Onboarding),
|
||||
defaultValue: null);
|
||||
|
||||
public static Func<OnboardingStep, CancellationToken, Task>? GetOnLeaving(BindableObject view) =>
|
||||
(Func<OnboardingStep, CancellationToken, Task>?)view.GetValue(OnLeavingProperty);
|
||||
|
||||
public static void SetOnLeaving(BindableObject view, Func<OnboardingStep, CancellationToken, Task>? value) =>
|
||||
view.SetValue(OnLeavingProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Helper to get the effective step key, falling back to AutomationId.
|
||||
/// </summary>
|
||||
|
||||
@ -27,6 +27,8 @@ public class OnboardingHost : Grid, IDisposable
|
||||
private CalloutCorner? _currentCalloutCorner;
|
||||
private readonly SemaphoreSlim _tourLock = new(1, 1);
|
||||
private bool _isDisposed;
|
||||
private readonly Dictionary<string, Func<OnboardingStep, CancellationToken, Task>> _enteringActions = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Func<OnboardingStep, CancellationToken, Task>> _leavingActions = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
#region Bindable Properties
|
||||
|
||||
@ -351,6 +353,24 @@ public class OnboardingHost : Grid, IDisposable
|
||||
/// </summary>
|
||||
public event EventHandler? IntroDismissed;
|
||||
|
||||
/// <summary>
|
||||
/// Raised before entering a step, allowing async preparation or skipping.
|
||||
/// Actions execute before the spotlight animates to the new position.
|
||||
/// Set e.Skip = true to skip this step.
|
||||
/// </summary>
|
||||
public event Func<object?, StepActionEventArgs, Task>? StepEntering;
|
||||
|
||||
/// <summary>
|
||||
/// Raised after a step is fully visible (after animations complete).
|
||||
/// </summary>
|
||||
public event EventHandler<StepActionEventArgs>? StepEntered;
|
||||
|
||||
/// <summary>
|
||||
/// Raised before leaving the current step.
|
||||
/// Actions execute before navigating to the next/previous step.
|
||||
/// </summary>
|
||||
public event Func<object?, StepActionEventArgs, Task>? StepLeaving;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read-only Properties
|
||||
@ -384,6 +404,65 @@ public class OnboardingHost : Grid, IDisposable
|
||||
|
||||
#endregion
|
||||
|
||||
#region Step Action Registration
|
||||
|
||||
/// <summary>
|
||||
/// Registers an action to execute when entering a specific step (by StepKey).
|
||||
/// The action runs before the spotlight animates to the step.
|
||||
/// </summary>
|
||||
/// <param name="stepKey">The StepKey of the target step.</param>
|
||||
/// <param name="action">The async action to execute. Receives the step and cancellation token.</param>
|
||||
public void RegisterStepEnteringAction(string stepKey, Func<OnboardingStep, CancellationToken, Task> action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stepKey);
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
_enteringActions[stepKey] = action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an action to execute when leaving a specific step (by StepKey).
|
||||
/// The action runs before navigating away from the step.
|
||||
/// </summary>
|
||||
/// <param name="stepKey">The StepKey of the target step.</param>
|
||||
/// <param name="action">The async action to execute. Receives the step and cancellation token.</param>
|
||||
public void RegisterStepLeavingAction(string stepKey, Func<OnboardingStep, CancellationToken, Task> action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stepKey);
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
_leavingActions[stepKey] = action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a registered entering action for a step.
|
||||
/// </summary>
|
||||
/// <param name="stepKey">The StepKey of the target step.</param>
|
||||
/// <returns>True if the action was removed, false if no action was registered.</returns>
|
||||
public bool UnregisterStepEnteringAction(string stepKey)
|
||||
{
|
||||
return _enteringActions.Remove(stepKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a registered leaving action for a step.
|
||||
/// </summary>
|
||||
/// <param name="stepKey">The StepKey of the target step.</param>
|
||||
/// <returns>True if the action was removed, false if no action was registered.</returns>
|
||||
public bool UnregisterStepLeavingAction(string stepKey)
|
||||
{
|
||||
return _leavingActions.Remove(stepKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all registered step actions.
|
||||
/// </summary>
|
||||
public void ClearStepActions()
|
||||
{
|
||||
_enteringActions.Clear();
|
||||
_leavingActions.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public OnboardingHost()
|
||||
{
|
||||
IsVisible = false;
|
||||
@ -798,6 +877,131 @@ public class OnboardingHost : Grid, IDisposable
|
||||
return await _tourCompletionSource.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all entering actions for a step (event, attached property, registered action).
|
||||
/// Returns a StepActionEventArgs that may have Skip set to true.
|
||||
/// </summary>
|
||||
private async Task<StepActionEventArgs> ExecuteStepEnteringActionsAsync(
|
||||
OnboardingStep step,
|
||||
int stepIndex,
|
||||
int previousIndex,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var isForward = previousIndex < stepIndex || previousIndex < 0;
|
||||
var eventArgs = new StepActionEventArgs(
|
||||
step, stepIndex, _steps.Count, previousIndex, isForward, cancellationToken);
|
||||
|
||||
// 1. Fire StepEntering event (allows Skip)
|
||||
if (StepEntering != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await StepEntering.Invoke(this, eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[OnboardingHost] StepEntering event handler failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// If skipped via event, don't run other actions
|
||||
if (eventArgs.Skip)
|
||||
return eventArgs;
|
||||
|
||||
// 2. Execute attached property action (OnEntering on the element)
|
||||
if (step.OnEntering != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await step.OnEntering(step, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[OnboardingHost] OnEntering attached action failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Execute registered action (by StepKey)
|
||||
if (!string.IsNullOrEmpty(step.StepKey) && _enteringActions.TryGetValue(step.StepKey, out var registeredAction))
|
||||
{
|
||||
try
|
||||
{
|
||||
await registeredAction(step, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[OnboardingHost] Registered entering action failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return eventArgs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all leaving actions for a step (event, attached property, registered action).
|
||||
/// </summary>
|
||||
private async Task ExecuteStepLeavingActionsAsync(
|
||||
OnboardingStep step,
|
||||
int stepIndex,
|
||||
int nextIndex,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var isForward = nextIndex > stepIndex;
|
||||
var eventArgs = new StepActionEventArgs(
|
||||
step, stepIndex, _steps.Count, stepIndex, isForward, cancellationToken);
|
||||
|
||||
// 1. Fire StepLeaving event
|
||||
if (StepLeaving != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await StepLeaving.Invoke(this, eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[OnboardingHost] StepLeaving event handler failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Execute attached property action (OnLeaving on the element)
|
||||
if (step.OnLeaving != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await step.OnLeaving(step, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[OnboardingHost] OnLeaving attached action failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Execute registered action (by StepKey)
|
||||
if (!string.IsNullOrEmpty(step.StepKey) && _leavingActions.TryGetValue(step.StepKey, out var registeredAction))
|
||||
{
|
||||
try
|
||||
{
|
||||
await registeredAction(step, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[OnboardingHost] Registered leaving action failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires the StepEntered event after animations complete.
|
||||
/// </summary>
|
||||
private void RaiseStepEntered(OnboardingStep step, int stepIndex, int previousIndex)
|
||||
{
|
||||
var isForward = previousIndex < stepIndex || previousIndex < 0;
|
||||
var eventArgs = new StepActionEventArgs(
|
||||
step, stepIndex, _steps.Count, previousIndex, isForward);
|
||||
|
||||
StepEntered?.Invoke(this, eventArgs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to a specific step by index.
|
||||
/// </summary>
|
||||
@ -812,9 +1016,40 @@ public class OnboardingHost : Grid, IDisposable
|
||||
}
|
||||
|
||||
var previousIndex = _currentIndex;
|
||||
var step = _steps[index];
|
||||
|
||||
// Execute leaving actions for current step (if any)
|
||||
if (previousIndex >= 0 && previousIndex < _steps.Count)
|
||||
{
|
||||
var previousStep = _steps[previousIndex];
|
||||
await ExecuteStepLeavingActionsAsync(previousStep, previousIndex, index).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Execute entering actions for new step (before UI updates)
|
||||
var enteringArgs = await ExecuteStepEnteringActionsAsync(step, index, previousIndex).ConfigureAwait(false);
|
||||
|
||||
// Handle skip - move to next or previous step
|
||||
if (enteringArgs.Skip)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[OnboardingHost] Step {index} skipped via StepEntering event");
|
||||
var isForward = previousIndex < index || previousIndex < 0;
|
||||
var nextIndex = isForward ? index + 1 : index - 1;
|
||||
|
||||
if (nextIndex >= 0 && nextIndex < _steps.Count)
|
||||
{
|
||||
await GoToStepAsync(nextIndex).ConfigureAwait(false);
|
||||
}
|
||||
else if (isForward)
|
||||
{
|
||||
// No more steps forward, complete tour
|
||||
await CompleteTourAsync().ConfigureAwait(false);
|
||||
}
|
||||
// If going backward with no more steps, stay at current
|
||||
return;
|
||||
}
|
||||
|
||||
_currentIndex = index;
|
||||
System.Diagnostics.Debug.WriteLine($"[OnboardingHost] GoToStepAsync: previousIndex={previousIndex}, new _currentIndex={_currentIndex}");
|
||||
var step = _steps[index];
|
||||
|
||||
// Ensure element is visible (scroll if needed) - with timeout to prevent hanging
|
||||
try
|
||||
@ -899,7 +1134,10 @@ public class OnboardingHost : Grid, IDisposable
|
||||
}
|
||||
_cornerNavigator.UpdateAutoPlacement(bounds, new Size(Width, Height), calloutBounds, _currentCalloutCorner);
|
||||
|
||||
// Raise event
|
||||
// Raise StepEntered event (after animations complete)
|
||||
RaiseStepEntered(step, index, previousIndex);
|
||||
|
||||
// Raise legacy StepChanged event
|
||||
StepChanged?.Invoke(this, new OnboardingStepEventArgs(step, index, _steps.Count));
|
||||
|
||||
// Start auto-advance timer if enabled
|
||||
@ -1358,9 +1596,39 @@ public class OnboardingHost : Grid, IDisposable
|
||||
|
||||
var config = TourConfiguration.Default;
|
||||
var previousIndex = _currentIndex;
|
||||
_currentIndex = index;
|
||||
var step = _steps[index];
|
||||
|
||||
// Execute leaving actions for current step (if any)
|
||||
if (previousIndex >= 0 && previousIndex < _steps.Count)
|
||||
{
|
||||
var previousStep = _steps[previousIndex];
|
||||
await ExecuteStepLeavingActionsAsync(previousStep, previousIndex, index, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Execute entering actions for new step (before UI updates)
|
||||
var enteringArgs = await ExecuteStepEnteringActionsAsync(step, index, previousIndex, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Handle skip - move to next or previous step
|
||||
if (enteringArgs.Skip)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[OnboardingHost] Step {index} skipped via StepEntering event (internal)");
|
||||
var isForward = previousIndex < index || previousIndex < 0;
|
||||
var nextIndex = isForward ? index + 1 : index - 1;
|
||||
|
||||
if (nextIndex >= 0 && nextIndex < _steps.Count)
|
||||
{
|
||||
await GoToStepInternalAsync(nextIndex, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (isForward)
|
||||
{
|
||||
// No more steps forward, complete tour
|
||||
await CompleteTourAsync().ConfigureAwait(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_currentIndex = index;
|
||||
|
||||
// Ensure element is visible (scroll if needed) - with timeout to prevent hanging
|
||||
try
|
||||
{
|
||||
@ -1448,7 +1716,10 @@ public class OnboardingHost : Grid, IDisposable
|
||||
}
|
||||
_cornerNavigator.UpdateAutoPlacement(bounds, new Size(Width, Height), calloutBounds, _currentCalloutCorner);
|
||||
|
||||
// Raise event
|
||||
// Raise StepEntered event (after animations complete)
|
||||
RaiseStepEntered(step, index, previousIndex);
|
||||
|
||||
// Raise legacy StepChanged event
|
||||
StepChanged?.Invoke(this, new OnboardingStepEventArgs(step, index, _steps.Count));
|
||||
|
||||
// Start auto-advance timer if enabled
|
||||
@ -1843,3 +2114,62 @@ public class OnboardingStepEventArgs : EventArgs
|
||||
TotalSteps = totalSteps;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for step action events (StepEntering, StepLeaving).
|
||||
/// Allows actions to skip steps or perform async preparation.
|
||||
/// </summary>
|
||||
public class StepActionEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The step being entered or left.
|
||||
/// </summary>
|
||||
public OnboardingStep Step { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The index of the step (0-based).
|
||||
/// </summary>
|
||||
public int StepIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of steps in the tour.
|
||||
/// </summary>
|
||||
public int TotalSteps { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The index of the previous step (-1 if this is the first step).
|
||||
/// </summary>
|
||||
public int PreviousStepIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether navigation is moving forward (true) or backward (false).
|
||||
/// </summary>
|
||||
public bool IsForward { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to skip this step and move to the next one.
|
||||
/// Only applies to StepEntering event.
|
||||
/// </summary>
|
||||
public bool Skip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token for the current operation.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
public StepActionEventArgs(
|
||||
OnboardingStep step,
|
||||
int stepIndex,
|
||||
int totalSteps,
|
||||
int previousStepIndex,
|
||||
bool isForward,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Step = step;
|
||||
StepIndex = stepIndex;
|
||||
TotalSteps = totalSteps;
|
||||
PreviousStepIndex = previousStepIndex;
|
||||
IsForward = isForward;
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +65,18 @@ public class OnboardingStep
|
||||
/// </summary>
|
||||
public SpotlightTapBehavior TapBehavior { get; init; } = SpotlightTapBehavior.None;
|
||||
|
||||
/// <summary>
|
||||
/// Action to execute when entering this step.
|
||||
/// Runs before the spotlight animates to allow UI preparation.
|
||||
/// </summary>
|
||||
public Func<OnboardingStep, CancellationToken, Task>? OnEntering { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Action to execute when leaving this step.
|
||||
/// Runs before navigating to the next/previous step.
|
||||
/// </summary>
|
||||
public Func<OnboardingStep, CancellationToken, Task>? OnLeaving { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an OnboardingStep from a tagged VisualElement.
|
||||
/// </summary>
|
||||
@ -83,7 +95,9 @@ public class OnboardingStep
|
||||
SpotlightShape = Onboarding.GetSpotlightShape(element),
|
||||
SpotlightPadding = Onboarding.GetSpotlightPadding(element),
|
||||
SpotlightCornerRadius = Onboarding.GetSpotlightCornerRadius(element),
|
||||
TapBehavior = Onboarding.GetTapBehavior(element)
|
||||
TapBehavior = Onboarding.GetTapBehavior(element),
|
||||
OnEntering = Onboarding.GetOnEntering(element),
|
||||
OnLeaving = Onboarding.GetOnLeaving(element)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
92
README.md
92
README.md
@ -332,17 +332,109 @@ TourHost.SkipTour();
|
||||
TourHost.StopTour();
|
||||
```
|
||||
|
||||
## Step Actions
|
||||
|
||||
Execute custom code when entering or leaving tour steps. Perfect for UI preparation like switching tabs, opening drawers, or triggering animations before the spotlight appears.
|
||||
|
||||
### Attached Property (Per-Element)
|
||||
|
||||
```csharp
|
||||
// In code-behind, attach action directly to the element
|
||||
Onboarding.SetOnEntering(SettingsTab, async (step, cancellationToken) =>
|
||||
{
|
||||
// Switch to the settings tab before spotlight appears
|
||||
MainTabView.SelectedIndex = 2;
|
||||
await Task.Delay(100); // Wait for tab transition
|
||||
});
|
||||
|
||||
Onboarding.SetOnLeaving(SettingsTab, async (step, cancellationToken) =>
|
||||
{
|
||||
// Cleanup when leaving this step
|
||||
MainTabView.SelectedIndex = 0;
|
||||
});
|
||||
```
|
||||
|
||||
### Centralized Registration (By StepKey)
|
||||
|
||||
```csharp
|
||||
// Register actions by step key - keeps tour logic organized
|
||||
TourHost.RegisterStepEnteringAction("drawer-step", async (step, token) =>
|
||||
{
|
||||
Shell.Current.FlyoutIsPresented = true;
|
||||
await Task.Delay(250); // Wait for drawer animation
|
||||
});
|
||||
|
||||
TourHost.RegisterStepEnteringAction("settings-tab", async (step, token) =>
|
||||
{
|
||||
SettingsTabButton.IsSelected = true;
|
||||
});
|
||||
|
||||
// Remove registered actions when no longer needed
|
||||
TourHost.UnregisterStepEnteringAction("drawer-step");
|
||||
TourHost.ClearStepActions(); // Clear all
|
||||
```
|
||||
|
||||
### Step Action Events
|
||||
|
||||
```csharp
|
||||
// Global event - fires for every step (useful for analytics, logging, conditional skipping)
|
||||
TourHost.StepEntering += async (sender, e) =>
|
||||
{
|
||||
// Skip premium features for non-premium users
|
||||
if (e.Step.StepKey == "premium-feature" && !currentUser.IsPremium)
|
||||
{
|
||||
e.Skip = true; // Skip this step, move to next
|
||||
return;
|
||||
}
|
||||
|
||||
// Log analytics
|
||||
Analytics.Track("tour_step_entering", e.Step.StepKey);
|
||||
};
|
||||
|
||||
// Fires after step is fully visible (animations complete)
|
||||
TourHost.StepEntered += (sender, e) =>
|
||||
{
|
||||
Console.WriteLine($"Now viewing step: {e.Step.Title}");
|
||||
};
|
||||
|
||||
// Fires before leaving current step
|
||||
TourHost.StepLeaving += async (sender, e) =>
|
||||
{
|
||||
// Perform cleanup before navigating away
|
||||
await SaveStepProgressAsync(e.StepIndex);
|
||||
};
|
||||
```
|
||||
|
||||
### Execution Order
|
||||
|
||||
Actions execute in this order when navigating to a step:
|
||||
|
||||
1. `StepLeaving` event + actions (previous step)
|
||||
2. `StepEntering` event (can set `Skip = true`)
|
||||
3. `OnEntering` attached property action
|
||||
4. Registered entering action (by StepKey)
|
||||
5. Scroll target into view & animate spotlight
|
||||
6. `StepEntered` event
|
||||
|
||||
## Events
|
||||
|
||||
```csharp
|
||||
// Tour lifecycle events
|
||||
TourHost.TourStarted += (s, e) => { /* Tour began */ };
|
||||
TourHost.TourCompleted += (s, e) => { /* User finished all steps */ };
|
||||
TourHost.TourSkipped += (s, e) => { /* User skipped */ };
|
||||
TourHost.TourEnded += (s, e) => { /* Tour ended (any reason) */ };
|
||||
|
||||
// Step navigation events
|
||||
TourHost.StepEntering += async (s, e) => { /* Before entering step - can set e.Skip = true */ };
|
||||
TourHost.StepEntered += (s, e) => { /* After step is visible */ };
|
||||
TourHost.StepLeaving += async (s, e) => { /* Before leaving step */ };
|
||||
TourHost.StepChanged += (s, e) =>
|
||||
{
|
||||
Console.WriteLine($"Step {e.StepIndex + 1}/{e.TotalSteps}: {e.Step.Title}");
|
||||
};
|
||||
|
||||
// Intro events
|
||||
TourHost.IntroShown += (s, e) => { /* Intro view displayed */ };
|
||||
TourHost.IntroDismissed += (s, e) => { /* Intro view dismissed */ };
|
||||
```
|
||||
|
||||
@ -77,4 +77,11 @@
|
||||
Route="AnimationsPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="Step Actions" Icon="{OnPlatform Default=icon_action.png}">
|
||||
<ShellContent
|
||||
Title="Step Actions"
|
||||
ContentTemplate="{DataTemplate pages:StepActionsPage}"
|
||||
Route="StepActionsPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
</Shell>
|
||||
|
||||
@ -150,6 +150,16 @@
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#009688" Padding="15" CornerRadius="8">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OnStepActionsTapped" />
|
||||
</Frame.GestureRecognizers>
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Step Actions" FontAttributes="Bold" TextColor="White" />
|
||||
<Label Text="Execute code when entering/leaving steps (tab switching, drawers)" TextColor="White" FontSize="12" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<BoxView HeightRequest="10" Color="Transparent" />
|
||||
|
||||
<Label
|
||||
|
||||
@ -91,4 +91,7 @@ public partial class MainPage : ContentPage
|
||||
|
||||
private async void OnAnimationsTapped(object? sender, EventArgs e)
|
||||
=> await Shell.Current.GoToAsync("//AnimationsPage");
|
||||
|
||||
private async void OnStepActionsTapped(object? sender, EventArgs e)
|
||||
=> await Shell.Current.GoToAsync("//StepActionsPage");
|
||||
}
|
||||
|
||||
245
Test.SpotlightTour/Pages/StepActionsPage.xaml
Normal file
245
Test.SpotlightTour/Pages/StepActionsPage.xaml
Normal file
@ -0,0 +1,245 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:onboard="clr-namespace:MarketAlly.SpotlightTour.Maui;assembly=MarketAlly.SpotlightTour.Maui"
|
||||
x:Class="Test.SpotlightTour.Pages.StepActionsPage"
|
||||
Title="Step Actions">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Header -->
|
||||
<Border Grid.Row="0" Padding="20" StrokeThickness="0"
|
||||
BackgroundColor="{AppThemeBinding Light={StaticResource Primary}, Dark=#1E3A5F}">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label
|
||||
Text="Step Actions Demo"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold"
|
||||
TextColor="White"
|
||||
onboard:Onboarding.StepKey="header"
|
||||
onboard:Onboarding.Title="Step Actions"
|
||||
onboard:Onboarding.Description="This demo shows how to execute custom code when entering or leaving tour steps."
|
||||
onboard:Onboarding.Order="1" />
|
||||
|
||||
<Label
|
||||
Text="Watch tabs switch automatically as the tour progresses!"
|
||||
TextColor="{AppThemeBinding Light=#FFFFFFCC, Dark=#FFFFFFAA}" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<!-- Main Content with Tabs -->
|
||||
<Grid Grid.Row="1">
|
||||
|
||||
<!-- Tab Bar -->
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<HorizontalStackLayout
|
||||
x:Name="TabBar"
|
||||
Spacing="0"
|
||||
BackgroundColor="{AppThemeBinding Light=#F5F5F5, Dark=#2C2C2E}"
|
||||
onboard:Onboarding.StepKey="tab-bar"
|
||||
onboard:Onboarding.Title="Tab Navigation"
|
||||
onboard:Onboarding.Description="The tour will automatically switch tabs to show you different features."
|
||||
onboard:Onboarding.Order="2">
|
||||
|
||||
<Button
|
||||
x:Name="HomeTabBtn"
|
||||
Text="Home"
|
||||
BackgroundColor="{StaticResource Primary}"
|
||||
TextColor="White"
|
||||
CornerRadius="0"
|
||||
WidthRequest="120"
|
||||
Clicked="OnHomeTabClicked" />
|
||||
|
||||
<Button
|
||||
x:Name="SettingsTabBtn"
|
||||
Text="Settings"
|
||||
BackgroundColor="{AppThemeBinding Light=#E0E0E0, Dark=#3A3A3C}"
|
||||
TextColor="{AppThemeBinding Light=#333333, Dark=#FFFFFF}"
|
||||
CornerRadius="0"
|
||||
WidthRequest="120"
|
||||
Clicked="OnSettingsTabClicked" />
|
||||
|
||||
<Button
|
||||
x:Name="ProfileTabBtn"
|
||||
Text="Profile"
|
||||
BackgroundColor="{AppThemeBinding Light=#E0E0E0, Dark=#3A3A3C}"
|
||||
TextColor="{AppThemeBinding Light=#333333, Dark=#FFFFFF}"
|
||||
CornerRadius="0"
|
||||
WidthRequest="120"
|
||||
Clicked="OnProfileTabClicked" />
|
||||
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<Grid Grid.Row="1" BackgroundColor="{AppThemeBinding Light=#FFFFFF, Dark=#1C1C1E}">
|
||||
|
||||
<!-- Home Tab Content -->
|
||||
<VerticalStackLayout
|
||||
x:Name="HomeContent"
|
||||
Padding="20"
|
||||
Spacing="15"
|
||||
IsVisible="True">
|
||||
|
||||
<Label Text="Home" FontSize="20" FontAttributes="Bold" />
|
||||
|
||||
<Border
|
||||
Padding="15"
|
||||
StrokeShape="RoundRectangle 8"
|
||||
BackgroundColor="{AppThemeBinding Light=#E3F2FD, Dark=#1A2F3D}"
|
||||
Stroke="{AppThemeBinding Light=Transparent, Dark=#2A4A5C}"
|
||||
onboard:Onboarding.StepKey="home-card"
|
||||
onboard:Onboarding.Title="Home Dashboard"
|
||||
onboard:Onboarding.Description="This is your home dashboard. Next, we'll automatically switch to the Settings tab!"
|
||||
onboard:Onboarding.Order="3">
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Welcome back!" FontAttributes="Bold" />
|
||||
<Label Text="Your dashboard shows key metrics and quick actions."
|
||||
TextColor="{AppThemeBinding Light=Gray, Dark=#ABABAB}" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Settings Tab Content -->
|
||||
<VerticalStackLayout
|
||||
x:Name="SettingsContent"
|
||||
Padding="20"
|
||||
Spacing="15"
|
||||
IsVisible="False">
|
||||
|
||||
<Label Text="Settings" FontSize="20" FontAttributes="Bold" />
|
||||
|
||||
<Border
|
||||
Padding="15"
|
||||
StrokeShape="RoundRectangle 8"
|
||||
x:Name="SettingsCard"
|
||||
BackgroundColor="{AppThemeBinding Light=#FFF3E0, Dark=#3D2E1A}"
|
||||
Stroke="{AppThemeBinding Light=Transparent, Dark=#5C4A2A}"
|
||||
onboard:Onboarding.StepKey="settings-card"
|
||||
onboard:Onboarding.Title="Settings Panel"
|
||||
onboard:Onboarding.Description="The tour switched to Settings automatically! OnEntering action changed the tab."
|
||||
onboard:Onboarding.Order="4">
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="App Settings" FontAttributes="Bold" />
|
||||
<Label Text="Configure notifications, theme, and preferences."
|
||||
TextColor="{AppThemeBinding Light=Gray, Dark=#ABABAB}" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Padding="15"
|
||||
StrokeShape="RoundRectangle 8"
|
||||
BackgroundColor="{AppThemeBinding Light=#FFEBEE, Dark=#3D1A1A}"
|
||||
Stroke="{AppThemeBinding Light=Transparent, Dark=#5C2A2A}"
|
||||
onboard:Onboarding.StepKey="premium-feature"
|
||||
onboard:Onboarding.Title="Premium Feature"
|
||||
onboard:Onboarding.Description="This step can be conditionally skipped using the StepEntering event."
|
||||
onboard:Onboarding.Order="5">
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Premium Settings" FontAttributes="Bold"
|
||||
TextColor="{AppThemeBinding Light=#D32F2F, Dark=#EF5350}" />
|
||||
<Label Text="Advanced features for premium users."
|
||||
TextColor="{AppThemeBinding Light=Gray, Dark=#ABABAB}" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Profile Tab Content -->
|
||||
<VerticalStackLayout
|
||||
x:Name="ProfileContent"
|
||||
Padding="20"
|
||||
Spacing="15"
|
||||
IsVisible="False">
|
||||
|
||||
<Label Text="Profile" FontSize="20" FontAttributes="Bold" />
|
||||
|
||||
<Border
|
||||
Padding="15"
|
||||
StrokeShape="RoundRectangle 8"
|
||||
BackgroundColor="{AppThemeBinding Light=#E8F5E9, Dark=#1B3D1F}"
|
||||
Stroke="{AppThemeBinding Light=Transparent, Dark=#2E5432}"
|
||||
onboard:Onboarding.StepKey="profile-card"
|
||||
onboard:Onboarding.Title="Your Profile"
|
||||
onboard:Onboarding.Description="Finally, we switched to the Profile tab. This is the last step!"
|
||||
onboard:Onboarding.Order="6">
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="User Profile" FontAttributes="Bold" />
|
||||
<Label Text="View and edit your profile information."
|
||||
TextColor="{AppThemeBinding Light=Gray, Dark=#ABABAB}" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
</VerticalStackLayout>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Side Panel (Drawer simulation) -->
|
||||
<Border
|
||||
x:Name="SidePanel"
|
||||
Padding="20"
|
||||
StrokeThickness="0"
|
||||
IsVisible="False"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Fill"
|
||||
WidthRequest="280"
|
||||
BackgroundColor="{AppThemeBinding Light=White, Dark=#2C2C2E}"
|
||||
Shadow="{Shadow Brush=Black, Opacity=0.3, Offset='5,0'}">
|
||||
<VerticalStackLayout Spacing="15">
|
||||
<Label Text="Side Panel" FontSize="18" FontAttributes="Bold" />
|
||||
<Label Text="This panel was opened by a step action!"
|
||||
TextColor="{AppThemeBinding Light=Gray, Dark=#ABABAB}" />
|
||||
<Button Text="Close" Clicked="OnClosePanelClicked"
|
||||
BackgroundColor="{AppThemeBinding Light=#EEEEEE, Dark=#3A3A3C}"
|
||||
TextColor="{AppThemeBinding Light=#333333, Dark=#FFFFFF}" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Footer with Controls -->
|
||||
<Border Grid.Row="2" Padding="20" StrokeThickness="0"
|
||||
BackgroundColor="{AppThemeBinding Light=#F5F5F5, Dark=#2C2C2E}">
|
||||
<VerticalStackLayout Spacing="12">
|
||||
|
||||
<HorizontalStackLayout Spacing="10" HorizontalOptions="Center">
|
||||
<Switch x:Name="SkipPremiumSwitch" IsToggled="False" />
|
||||
<Label Text="Skip Premium Step" VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<Button
|
||||
x:Name="StartTourBtn"
|
||||
Text="Start Step Actions Tour"
|
||||
Clicked="OnStartTourClicked"
|
||||
BackgroundColor="{AppThemeBinding Light={StaticResource Primary}, Dark=#1E88E5}"
|
||||
TextColor="White"
|
||||
FontAttributes="Bold" />
|
||||
|
||||
<Border Padding="10" StrokeShape="RoundRectangle 6"
|
||||
BackgroundColor="{AppThemeBinding Light=#FAFAFA, Dark=#1C1C1E}"
|
||||
Stroke="{AppThemeBinding Light=#E0E0E0, Dark=#3A3A3C}">
|
||||
<Label
|
||||
x:Name="LogLabel"
|
||||
Text="Action log will appear here..."
|
||||
FontSize="11"
|
||||
FontFamily="Consolas"
|
||||
TextColor="{AppThemeBinding Light=#666666, Dark=#ABABAB}"
|
||||
LineBreakMode="WordWrap" />
|
||||
</Border>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<!-- Onboarding Host -->
|
||||
<onboard:OnboardingHost
|
||||
x:Name="TourHost"
|
||||
Grid.RowSpan="3"
|
||||
ShowCornerNavigator="True"
|
||||
ShowSkipButton="True"
|
||||
DimOpacity="0.75"
|
||||
Theme="System" />
|
||||
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
148
Test.SpotlightTour/Pages/StepActionsPage.xaml.cs
Normal file
148
Test.SpotlightTour/Pages/StepActionsPage.xaml.cs
Normal file
@ -0,0 +1,148 @@
|
||||
using MarketAlly.SpotlightTour.Maui;
|
||||
|
||||
namespace Test.SpotlightTour.Pages;
|
||||
|
||||
public partial class StepActionsPage : ContentPage
|
||||
{
|
||||
private readonly List<string> _actionLog = new();
|
||||
|
||||
public StepActionsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
SetupStepActions();
|
||||
}
|
||||
|
||||
private void SetupStepActions()
|
||||
{
|
||||
// Method 1: Register actions by StepKey (centralized approach)
|
||||
TourHost.RegisterStepEnteringAction("settings-card", async (step, token) =>
|
||||
{
|
||||
LogAction("Entering settings-card: Switching to Settings tab...");
|
||||
|
||||
// Switch to Settings tab before spotlight appears
|
||||
await MainThread.InvokeOnMainThreadAsync(() => SwitchToTab(1));
|
||||
await Task.Delay(150); // Wait for UI to update
|
||||
});
|
||||
|
||||
TourHost.RegisterStepEnteringAction("profile-card", async (step, token) =>
|
||||
{
|
||||
LogAction("Entering profile-card: Switching to Profile tab...");
|
||||
|
||||
// Switch to Profile tab
|
||||
await MainThread.InvokeOnMainThreadAsync(() => SwitchToTab(2));
|
||||
await Task.Delay(150);
|
||||
});
|
||||
|
||||
// Method 2: Attach action directly to element via attached property
|
||||
Onboarding.SetOnEntering(SettingsCard, async (step, token) =>
|
||||
{
|
||||
LogAction("OnEntering attached property executed for SettingsCard");
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Method 3: Use events for cross-cutting concerns
|
||||
TourHost.StepEntering += async (sender, e) =>
|
||||
{
|
||||
LogAction($"StepEntering event: {e.Step.StepKey} (index {e.StepIndex})");
|
||||
|
||||
// Conditional skip: Skip premium step if toggle is on
|
||||
if (e.Step.StepKey == "premium-feature" && SkipPremiumSwitch.IsToggled)
|
||||
{
|
||||
LogAction("Skipping premium-feature step (toggle is ON)");
|
||||
e.Skip = true;
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
};
|
||||
|
||||
TourHost.StepEntered += (sender, e) =>
|
||||
{
|
||||
LogAction($"StepEntered event: {e.Step.StepKey} is now visible");
|
||||
};
|
||||
|
||||
TourHost.StepLeaving += async (sender, e) =>
|
||||
{
|
||||
LogAction($"StepLeaving event: Leaving {e.Step.StepKey}");
|
||||
|
||||
// Example: Close side panel when leaving any step
|
||||
if (SidePanel.IsVisible)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(() => SidePanel.IsVisible = false);
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
};
|
||||
|
||||
// Tour lifecycle events
|
||||
TourHost.TourCompleted += (s, e) =>
|
||||
{
|
||||
LogAction("Tour completed!");
|
||||
MainThread.BeginInvokeOnMainThread(() => SwitchToTab(0)); // Reset to Home
|
||||
};
|
||||
|
||||
TourHost.TourSkipped += (s, e) =>
|
||||
{
|
||||
LogAction("Tour skipped");
|
||||
MainThread.BeginInvokeOnMainThread(() => SwitchToTab(0)); // Reset to Home
|
||||
};
|
||||
}
|
||||
|
||||
private void SwitchToTab(int index)
|
||||
{
|
||||
// Get theme-aware colors
|
||||
var isDark = Application.Current?.RequestedTheme == AppTheme.Dark;
|
||||
var activeColor = Color.FromArgb("#512BD4"); // Primary
|
||||
var inactiveColor = isDark ? Color.FromArgb("#3A3A3C") : Color.FromArgb("#E0E0E0");
|
||||
var activeTextColor = Colors.White;
|
||||
var inactiveTextColor = isDark ? Colors.White : Color.FromArgb("#333333");
|
||||
|
||||
// Update tab button styles
|
||||
HomeTabBtn.BackgroundColor = index == 0 ? activeColor : inactiveColor;
|
||||
HomeTabBtn.TextColor = index == 0 ? activeTextColor : inactiveTextColor;
|
||||
|
||||
SettingsTabBtn.BackgroundColor = index == 1 ? activeColor : inactiveColor;
|
||||
SettingsTabBtn.TextColor = index == 1 ? activeTextColor : inactiveTextColor;
|
||||
|
||||
ProfileTabBtn.BackgroundColor = index == 2 ? activeColor : inactiveColor;
|
||||
ProfileTabBtn.TextColor = index == 2 ? activeTextColor : inactiveTextColor;
|
||||
|
||||
// Show/hide content
|
||||
HomeContent.IsVisible = index == 0;
|
||||
SettingsContent.IsVisible = index == 1;
|
||||
ProfileContent.IsVisible = index == 2;
|
||||
}
|
||||
|
||||
private void LogAction(string message)
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
_actionLog.Add($"[{timestamp}] {message}");
|
||||
|
||||
// Keep only last 5 entries
|
||||
while (_actionLog.Count > 5)
|
||||
_actionLog.RemoveAt(0);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
LogLabel.Text = string.Join("\n", _actionLog);
|
||||
});
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[StepActions] {message}");
|
||||
}
|
||||
|
||||
private async void OnStartTourClicked(object? sender, EventArgs e)
|
||||
{
|
||||
_actionLog.Clear();
|
||||
LogAction("Starting tour...");
|
||||
|
||||
// Reset to Home tab
|
||||
SwitchToTab(0);
|
||||
|
||||
var result = await TourHost.StartTourAsync(this.Content);
|
||||
LogAction($"Tour ended with result: {result}");
|
||||
}
|
||||
|
||||
private void OnHomeTabClicked(object? sender, EventArgs e) => SwitchToTab(0);
|
||||
private void OnSettingsTabClicked(object? sender, EventArgs e) => SwitchToTab(1);
|
||||
private void OnProfileTabClicked(object? sender, EventArgs e) => SwitchToTab(2);
|
||||
private void OnClosePanelClicked(object? sender, EventArgs e) => SidePanel.IsVisible = false;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user