Caliburn.Micro
Caliburn.Micro copied to clipboard
Async Navigation Service
I've written an async version of INavigationService
and related classes which I'd like to contribute back to the project:
IAsyncNavigationService.cs
using System;
using System.Threading.Tasks;
using System.Windows.Navigation;
namespace MyApp.Mvvm.Navigation
{
/// <summary>
/// Temporary async version of <see cref="Caliburn.Micro.INavigationService"/>.
/// </summary>
public interface IAsyncNavigationService
{
#region Properties
/// <summary>
/// Indicates whether the navigator can navigate back.
/// </summary>
bool CanGoBack { get; }
/// <summary>
/// Indicates whether the navigator can navigate forward.
/// </summary>
bool CanGoForward { get; }
/// <summary>
/// The current content.
/// </summary>
object CurrentContent { get; }
#endregion
#region Events
/// <summary>
/// Raised after navigation.
/// </summary>
event NavigatedEventHandler Navigated;
/// <summary>
/// Raised prior to navigation.
/// </summary>
event NavigatingCancelEventHandler Navigating;
/// <summary>
/// Raised when navigation fails.
/// </summary>
event NavigationFailedEventHandler NavigationFailed;
/// <summary>
/// Raised when navigation is stopped.
/// </summary>
event NavigationStoppedEventHandler NavigationStopped;
/// <summary>
/// Raised when a fragment navigation occurs.
/// </summary>
event FragmentNavigationEventHandler FragmentNavigation;
#endregion
#region Methods
/// <summary>
/// Navigates to the view represented by the given view model.
/// </summary>
/// <param name="viewModel">The view model to navigate to.</param>
/// <param name="extraData">Extra data to populate the view model with.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
Task NavigateToViewModelAsync(Type viewModel, object extraData = null);
/// <summary>
/// Navigates to the view represented by the given view model.
/// </summary>
/// <typeparam name="TViewModel">The view model to navigate to.</typeparam>
/// <param name="extraData">Extra data to populate the view model with.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
Task NavigateToViewModelAsync<TViewModel>(object extraData = null);
/// <summary>
/// Stops the loading process.
/// </summary>
void StopLoading();
/// <summary>
/// Navigates back.
/// </summary>
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
Task GoBackAsync();
/// <summary>
/// Navigates forward.
/// </summary>
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
Task GoForwardAsync();
/// <summary>
/// Removes the most recent entry from the back stack.
/// </summary>
/// <returns> The entry that was removed. </returns>
JournalEntry RemoveBackEntry();
#endregion
}
}
AsyncNavigationHelper.cs
using Caliburn.Micro;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;
namespace MyApp.Mvvm.Navigation
{
/// <summary>
/// Builds a Uri in a strongly typed fashion, based on a ViewModel.
/// </summary>
/// <typeparam name="TViewModel"></typeparam>
public class NavigationHelper<TViewModel>
{
#region Fields
readonly Dictionary<string, object> m_QueryString = new Dictionary<string, object>();
IAsyncNavigationService m_NavigationService;
#endregion
#region Public Methods
/// <summary>
/// Adds a query string parameter to the Uri.
/// </summary>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <param name="property">The property.</param>
/// <param name="value">The property value.</param>
/// <returns>Itself</returns>
public NavigationHelper<TViewModel> WithParam<TValue>(Expression<Func<TViewModel, TValue>> property, TValue value)
{
if (value is ValueType || !ReferenceEquals(null, value))
{
m_QueryString[property.GetMemberInfo().Name] = value;
}
return this;
}
/// <summary>
/// Attaches a navigation servies to this builder.
/// </summary>
/// <param name="navigationService">The navigation service.</param>
/// <returns>Itself</returns>
public NavigationHelper<TViewModel> AttachTo(IAsyncNavigationService navigationService)
{
m_NavigationService = navigationService;
return this;
}
/// <summary>
/// Navigates to the Uri represented by this builder.
/// </summary>
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
public async Task NavigateAsync()
{
if (m_NavigationService == null)
{
throw new InvalidOperationException("Cannot navigate without attaching an IAsyncNavigationService. Call AttachTo first.");
}
await m_NavigationService.NavigateToViewModelAsync<TViewModel>(m_QueryString);
}
#endregion
}
}
AsyncNavigationExtensions.cs
namespace MyApp.Mvvm.Navigation
{
/// <summary>
/// Extension methods related to navigation.
/// </summary>
public static class NavigationExtensions
{
#region Properties
/// <summary>
/// Creates a Uri builder based on a view model type.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
/// <param name="navigationService">The navigation service.</param>
/// <returns>The builder.</returns>
public static NavigationHelper<TViewModel> For<TViewModel>(this IAsyncNavigationService navigationService)
{
return new NavigationHelper<TViewModel>().AttachTo(navigationService);
}
#endregion
}
}
@nigel-sampson Hopefully this will help with the release of v4.0.0.
@nigel-sampson I suggest to make the following async (As often other async Application services are called to retrieve the navigation status, which creates the risk of deadlocks).
/// <summary>
/// Indicates whether the navigator can navigate back.
/// </summary>
Task<bool> CanGoBack { get; }
/// <summary>
/// Indicates whether the navigator can navigate forward.
/// </summary>
Task<bool> CanGoForward { get; }
It's looking good, thanks. One thing I was considering was the trying to create a platform agnostic INavigationService
that could be shared across platforms with a specific implementation per platform.
A lot more work and also not sure if there's any demand. Thoughts?
@nigel-sampson I would greatly appreciate that as it would allow me to move my NavigatingConductor<T>
to a .NET Standard assembly.
@popcatalin81 I disagree with your suggestion, as I don't believe properties should ever be async. To quote MSDN Magazine:
The term “asynchronous property” is actually an oxymoron. Property getters should execute immediately and retrieve current values, not kick off background operations. This is likely one of the reasons the async keyword can’t be used on a property getter. If you find your design asking for an asynchronous property, consider some alternatives first. In particular, should the property actually be a method (or a command)? If the property getter needs to kick off a new asynchronous operation each time it’s accessed, that’s not a property at all.
IMHO in a properly designed navigation service, CanGoBack
and CanGoForward
should just retrieve cached values that are updated in NavigateToViewModelAsync()
, GoBackAsync()
, and GoForwardAsync()
.
@neilt6 Make it a method then. bool CanGoBackAsync()