MvvmLib.Wpf

  • Mvvm: ViewModelLocator, BindingProxy, etc.
  • Navigation: NavigationService, ConfigurableNavigationService
  • Data: ListCollectionViewEx, PagedList, PagedSource and commands ListCollectionViewCommands, PagedSourceCommands
  • Interactivity: CallMethodAction, SelectorSelectedItemsSyncBehavior, EventToCommandBehavior, etc.
  • Controls: AnimatingContentControl, TransitioningContentControl, TransitioningItemsControl: allow to animate content. NavigatableContentControl
  • Expressions: allows to create filters with Linq expressions.
  • Common: MvvmUtils

ViewModelLocator

Allows to resolve ViewModels for Views with AutoWireViewModel.

Default convention:

  • Views in Views namespace
  • View models in ViewModels namespace
  • View model name:
    • view name + "ViewModel" (example: ShellViewModel for Shell)
    • Or if the view name ends by "View": view name + "Model" (example: NavigationViewModel for NavigationView)

Change the convention

Example 1: with "View" and "ViewModel" namespaces

ViewModelLocationProvider.ChangeConvention((viewType) =>
{
    // "View" and "ViewModel" folders
    var prefix  = viewType.FullName.Replace(".View.", ".ViewModel.");
    var suffix = viewType.Name.EndsWith("View") ? "Model" : "ViewModel";
    var assemblyFullName = viewType.Assembly.FullName;

    var viewModelTypeName = string.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", prefix, suffix, assemblyFullName);
    return Type.GetType(viewModelTypeName);
});

Example 2: with multiple view folders

ViewModelLocationProvider.ChangeConvention(viewType =>
{
    // "Menus" or "Views"
    string prefix = viewType.FullName.Contains(".Menus.") ?
    viewType.FullName.Replace(".Menus.", ".ViewModels.")
    : viewType.FullName.Replace(".Views.", ".ViewModels.");

    var suffix = viewType.Name.EndsWith("View") ? "Model" : "ViewModel";
    var assemblyFullName = viewType.Assembly.FullName;

    var viewModelTypeName = string.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", prefix, suffix, assemblyFullName);
    return Type.GetType(viewModelTypeName);
});

Example 3: ViewModels in another assembly

ViewModelLocationProvider.ChangeConvention(viewType =>
{
    // Class Library "Lib"
    var viewModelName = viewType.Name.EndsWith("View") ? $"{viewType.Name}Model" : $"{viewType.Name}ViewModel";
    var viewModelTypeName = string.Format(CultureInfo.InvariantCulture, "Lib.ViewModels.{0}, Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", viewModelName);
    var viewModelType = Type.GetType(viewModelTypeName);

    return viewModelType;
});

Register a custom View Model for a view

ViewModelLocationProvider.Register<Shell, CustomViewModel>();

Or with a factory

ViewModelLocationProvider.Register<Shell>(() => new CustomViewModel());

AutoWireViewModel Attached property (Window, UserControl)

Example:

<Window x:Class="Sample.Views.Shell"
        ...
         xmlns:ml="http://mvvmlib.com/"
         ml:ViewModelLocator.AutoWireViewModel="True">

Note: the attached property can be omitted with a NavigationService (automatically tries to resolve the ViewModel for a View)

Simple, customizable, bindable, injectable with Mvvm support.

Method Description
Navigate Navigation by Uri or uri string to a registered view (or viewmodel)
Replace Allows to replace previous navigation entry
MoveTo Move to index or content
MoveToFirst Move to first page
MoveToLast Move to last page
MoveToPrevious Move to previous page
MoveToNext Move to next page
Clear clear history and content
Sync Allows to sync with another navigation service
Property Description
CanMoveToPrevious Checks if can move to previous entry
CanMoveToNext Checks if can move to next entry
Journal The navigation journal
Event Description
CanMoveToPreviousChanged Raised when the value changed
CanMoveToNextChanged Raised when the value changed
ContentChanged Raised on content changed
Navigated Raised on navigation end
Navigating Raised at beginning of navigation
NavigationFailed Raised on navigation failed (cancelled with activation guard for example)

Note: 1 navigation service per UIElement (or use NavigationBehavior for custom behaviors)

Bindable navigation service. Exemple with ContentControl

 <ContentControl Content="{Binding NavigationService.Content}"></ContentControl>

Register the navigation service with a IoC Container and inject in ViewModel

public class ShellViewModel
{
    public ShellViewModel(INavigationService navigationService)
    {
        NavigationService = navigationService;
        Commands = new NavigationServiceCommands(NavigationService);
        NavigationService.Navigate("HomeView");
    }

    public INavigationService NavigationService { get; }
    public NavigationServiceCommands Commands { get; }
}

Bind commands in view

<Button Command="{Binding Commands.MoveToFirstCommand}" Width="50">&lt;&lt;</Button>
<Button Command="{Binding Commands.MoveToPreviousCommand}" Width="50">&lt;/Button>
<Button Command="{Binding Commands.MoveToNextCommand}" Width="50">&gt;</Button>
<Button Command="{Binding Commands.MoveToLastCommand}" Width="50">&gt;&gt;</Button>
<Button Command="{Binding Commands.NavigateCommand}" CommandParameter="HomeView">Home</Button>
<!-- with parameter -->
<Button Command="{Binding Commands.NavigateCommand}" CommandParameter="ViewA?id=sample-id">View A</Button>>

In code

NavigationService.Navigate("ViewA");
// With parameters
NavigationService.Navigate("ViewA", new NavigateParameters
{
    { "id", 10 }
});
// note: the query parameters are added to the NavigateParameters
NavigationService.Navigate("ViewA?tag=sample", new NavigateParameters
{
    { "id", 10 }
});

ViewModel-First: It's possible to navigate to ViewModels

NavigationService.Navigate("ViewAViewModel");

Define a DataTemplate

<DataTemplate DataType="{x:Type viewModels:ViewAViewModel}">
    <views:ViewA />
</DataTemplate>

And register for navigation

// Example with IUnityContainer (Unity Bootstrapper)
Container.RegisterForNavigation<ViewAViewModel>();

Replace: example remove LoginView from navigation journal

public class LoginViewModel : ISupportNavigation
{
    private readonly IAuth _auth;
    private readonly INavigationService _navigationService;
    private DelegateCommand _loginCommand;
    private string _returnUrl;
    private INavigateParameters _parameters;

    public LoginViewModel(IAuth auth, INavigationService navigationService)
    {
        _auth = auth;
        _navigationService = navigationService;
    }

    public DelegateCommand LoginCommand
    {
        get
        {
            if (_loginCommand == null)
                _loginCommand = new DelegateCommand(Login);
            return _loginCommand;
        }
    }

    public bool IsNavigationTarget(NavigateContext context) => false;

    public void OnNavigatedFrom(NavigateContext context) { }

    public void OnNavigatedTo(NavigateContext context)
    {
        _returnUrl = context.Parameters.GetValue<string>("returnUrl");
        _parameters = context.GetOriginalParameters();
    }

    private void Login()
    {
        _auth.Login();
        _navigationService.Replace(_returnUrl, _parameters);
    }
}

Example: try to navigate to a protected resource and redirect to LoginView

public class ProtectedViewModel : BindableBase, ISupportNavigation, ISupportActivationGuard
{
    private readonly IAuth _auth;

    public ProtectedViewModel(IAuth auth)
    {
        _auth = auth;
    }

    public bool IsNavigationTarget(NavigateContext context) => false;

    public void OnNavigatedFrom(NavigateContext context)
    { }

    public void OnNavigatedTo(NavigateContext context)
    {
        var id = context.Parameters.GetValue<string>("id");
        // etc.
    }

    public void CanActivate(NavigateContext context, Action<bool> continuationCallback)
    {
        if (!_auth.IsLogged)
        {
            context.NavigationService.Navigate("LoginView?returnUrl=ProtectedView", context.Parameters);
            continuationCallback(false);
        }
        else
            continuationCallback(true);
    }
}

Registering views (and view models) for navigation

ConfigurableNavigationService is easy to configure (does not require to use a bootstrapper)

var navigationService = new ConfigurableNavigationService
{
    // Name ("PageKey", used in Uri or UriString) => Type
    PageAssociations = new Dictionary<string, Type>
    {
        { "HomeView", typeof(HomeView) },
        { "ViewA", typeof(ViewA) },
        // With ViewModel for example
        { "ViewBViewModel", typeof(ViewBViewModel) },
        // etc.
    }
};

Or use Scrutor to find and register all views of a namespace

var navigationService = new ConfigurableNavigationService();
navigationService.RegisterViewsInExactNamespaceOf<Shell>(); // or RegisterViewsInNamespaceOf

NavigationService: it's better to use a Bootstrapper (to register views for navigation) ...

Bootstrapper

Examples

With MvvmLib.Unity.Wpf

public class Bootstrapper : UnityBootstrapperBase
{
    protected override void RegisterTypes()
    {
        // Services
        Container.RegisterSingleton<INavigationService, NavigationService>();
        Container.RegisterSingleton<IAuth, FakeAuth>();

        // ViewModels
        Container.RegisterForNavigation<ViewBViewModel>();
        Container.RegisterViewModelsInExactNamespaceOf<ShellViewModel>();

        // Register For Navigation
        Container.RegisterForNavigationInNamespaceOf<Shell>(); // here
    }

    protected override Window CreateShell() => Container.Resolve<Shell>();
}

With MvvmLib.Microsoft.DependencyInjection.Extensions.Wpf

public class Bootstrapper : MicrosoftDependencyInjectionBootstrapperBase
{
    protected override void RegisterTypes()
    {
        // Services
        Services.AddSingleton<INavigationService, NavigationService>();
        Services.AddSingleton<IAuth, FakeAuth>();
        // ViewModels
        Services.RegisterForNavigation<ViewBViewModel>();
        Services.RegisterViewModelsInExactNamespaceOf<ShellViewModel>();

        // Register For Navigation
        Services.RegisterForNavigationInNamespaceOf<Shell>(); // here
    }

    protected override Window CreateShell() => ContainerLocator.Current.Resolve<Shell>();
}

With MvvmLib.IoC.Extensions.Wpf: all can be auto resolved

public class Bootstrapper : InjectorBootstrapperBase
{
    protected override void RegisterTypes()
    {
    }

    protected override Window CreateShell() => ContainerLocator.Current.Resolve<Shell>();
}

App.xaml: Replace StartupUri by Startup event

<Application x:Class="WpfApp1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApp1"
             Startup="Application_Startup">
    <Application.Resources>

    </Application.Resources>
</Application>

In code behind

public partial class App : Application
{
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        new Bootstrapper().Run();
    }
}

Create a Login Screen or a SplashScreen

public class Bootstrapper : UnityBootstrapperBase
{
    // etc.

    protected override Window CreateShell() => Container.Resolve<Shell>();

    protected override void ShowShell()
    {
        var window = new LoginOrSplashWindow();
        if (window.ShowDialog() == true)
            base.ShowShell();
        else
            Application.Current.Shutdown();
    }
}

Note: With a SplashScreenManager, BeforeCreatingShell method can be used.

ContainerLocator

Is used by MvvmLib to resolve all dependencies and is configured by the bootstrapper in background.

Manually (without Bootstrapper), example with Unity:

// Container is IUnityContainer
ContainerLocator.SetContainerLocationProvider(() => new UnityContainerLocationProvider(Container))

Resolve a dependency

var shell = ContainerLocator.Current.Resolve<Shell>();

Mvvm Support

Navigation

  • ISupportNavigation: IsNavigationTarget to manage the view resolved, OnNavigatedFrom and OnNavigatedTo methods
  • ISupportJournal: to not persist a view in journal
  • ISupportLoaded with ViewModel.EnableLoaded attached property on a View
  • ISupportActivation: notified when a view is active/ selected

Guards:

  • ISupportActivationGuard
  • ISupportDeactivationGuard

Example confirm navigation

public class ViewAViewModel : BindableBase, ISupportNavigation, ISupportActivationGuard, ISupportDeactivationGuard
{
    // etc.

    public void CanActivate(NavigateContext context, Action<bool> continuationCallback)
    {
        var can = MessageBox.Show($"Activate {nameof(ViewAViewModel)}?", "Confirmation", MessageBoxButton.OKCancel) == MessageBoxResult.OK;
        continuationCallback(can);
    }

    public void CanDeactivate(NavigateContext context, Action<bool> continuationCallback)
    {
        var can = MessageBox.Show($"Deactivate {nameof(ViewAViewModel)}?", "Confirmation", MessageBoxButton.OKCancel) == MessageBoxResult.OK;
        continuationCallback(can);
    }

    // returns always the same View/ViewModel if the pageKey of the uri is View
    public bool IsNavigationTarget(NavigateContext context) => context.PageKey == "ViewA";

    public void OnNavigatedFrom(NavigateContext context)  { }

    public void OnNavigatedTo(NavigateContext context)
    {
        var id = context.Parameters.GetValue<string>("id");
    }
}

IsNavigationTarget: Master Details Scenario Sample

public class UserDetailsViewModel : BindableBase, ISupportNavigation
{
    private User _user;
    public User User
    {
        get { return _user; }
        set { SetProperty(ref _user, value); }
    }

    public bool IsNavigationTarget(NavigateContext context)
    {
        if (context.Parameters.TryGetValue<int>("id", out var id))
            return id == User.Id;
        return false;
    }

    // etc.
}

Handle Loaded from ViewModel

Add the EnabledLoaded attached property on the View

<Window
        ...
        xmlns:ml="http://mvvmlib.com/"
        ml:ViewModel.EnableLoaded="True" />

Implement ISupportLoaded

public class ShellViewModel : ISupportLoaded
{
    public void OnLoaded()
    {
        // do something
    }
}

Change the Mvvm interfaces used by the Navigation Service

Example: replace to use Prism interfaces

public class PrismNavigationService : NavigationService
{
    #region Mvvm

    protected override NavigateContext CreateContext(Uri uri, INavigateParameters parameters, NavigateMode mode)
    {
        var context = new PrismNavigateContext(uri, parameters, mode)
        {
            NavigationService = this
        };
        return context;
    }

    protected override void TryAutoWireViewModel(object content)
    {
        if (content is FrameworkElement view && view.DataContext is null && PrismViewModelLocator.GetAutoWireViewModel(view) is null)
            PrismViewModelLocator.SetAutoWireViewModel(view, true);
    }

    protected override bool IsNavigationTarget(object content, NavigateContext context)
    {
        bool isNavigationTarget = false;
        MvvmUtils.ViewAndViewModelAction<INavigationAware>(content, ina => { isNavigationTarget = ina.IsNavigationTarget(context.AsPrism()); });
        return isNavigationTarget;
    }

    protected override void CanNavigate(object currentContent, object content, NavigateContext context, Action<bool> continuationCallback)
    {
        if (currentContent is IConfirmNavigationRequest)
            ((IConfirmNavigationRequest)currentContent).ConfirmNavigationRequest(context.AsPrism(), continuationCallback);
        else if (currentContent is FrameworkElement element && element.DataContext is IConfirmNavigationRequest)
            ((IConfirmNavigationRequest)element.DataContext).ConfirmNavigationRequest(context.AsPrism(), continuationCallback);
        else
            continuationCallback(true);
    }

    protected override bool PersistInJournal(object content)
    {
        bool persist = true;
        MvvmHelpers.ViewAndViewModelAction<IJournalAware>(content, ija => { persist &= ija.PersistInHistory(); });
        return persist;
    }

    protected override void SetActive(object content, bool isActive)
    {
        MvvmUtils.ViewAndViewModelAction<IActiveAware>(content, iaa => iaa.IsActive = isActive);
    }

    protected override void OnNavigatedFrom(object content, NavigateContext context)
    {
        MvvmUtils.ViewAndViewModelAction<INavigationAware>(content, x => x.OnNavigatedFrom(context.AsPrism()));
    }

    protected override void OnNavigatedTo(object content, NavigateContext context)
    {
        MvvmUtils.ViewAndViewModelAction<INavigationAware>(content, ina => ina.OnNavigatedTo(context.AsPrism()));
    }

    #endregion
}

public class PrismNavigateContext : NavigateContext
{
    public PrismNavigateContext(Uri uri, INavigateParameters parameters, NavigateMode mode) : base(uri, parameters, mode)
    {
        NavigationContext = CreatePrismNavigationContext();
    }

    private NavigationContext CreatePrismNavigationContext()
    {
        var parameters = Parameters.ToPrism();
        var navigationContext = new MvvmLibNavigationContext(this, Uri, parameters);
        return navigationContext;
    }

    public NavigationContext NavigationContext { get; }
}

public static class NavigateContextExtensions
{
    public static NavigationContext AsPrism(this NavigateContext navigateContext)
    {
        if (navigateContext is PrismNavigateContext prismNavigateContext)
            return prismNavigateContext.NavigationContext;
        return null;
    }
}

public static class INavigateParametersExtensions
{
    public static NavigationParameters ToPrism(this INavigateParameters parameters)
    {
        var navigationParameters = new NavigationParameters();
        foreach (var parameter in parameters)
            navigationParameters.Add(parameter.Key, parameter.Value);
        return navigationParameters;
    }
}

public class MvvmLibNavigationContext : NavigationContext
{
    public MvvmLibNavigationContext(NavigateContext context, Uri uri, NavigationParameters navigationParameters)
        : base(null, uri, navigationParameters)
    {
        Context = context;
        GetNavigationParameters(navigationParameters);
    }

    public INavigationService MvvmLibNavigationService => Context.NavigationService;
    public NavigateContext Context { get; }

    private void GetNavigationParameters(NavigationParameters navigationParameters)
    {
        if (navigationParameters != null)
        {
            foreach (KeyValuePair<string, object> navigationParameter in navigationParameters)
            {
                Parameters.Add(navigationParameter.Key, navigationParameter.Value);
            }
        }
    }
}

public static class NavigationContextExtensions
{
    public static T As<T>(this NavigationContext navigationContext) where T: NavigationContext
    {
        if (navigationContext is T objAsT)
            return objAsT;
        return default;
    }
}

Create a custom Navigation Service

Example with NavigationBehaviors and binding for Selectors (ListBox mutliple, TabControl, etc.)

public class CustomNavigationService : NavigationService, ICustomNavigationService
{
    private INavigationJournal _journal;
    private List<INavigationBehavior> _behaviors;

    public CustomNavigationService()
    {
        Views = new ObservableCollection<object>();
        ActiveViews = new ObservableCollection<object>();
        _behaviors = new List<INavigationBehavior>();
        _journal = new CustomJournal();
    }

    public ObservableCollection<object> Views { get; }
    public ObservableCollection<object> ActiveViews { get; }

    public override INavigationJournal Journal => _journal;

    public IEnumerable<INavigationBehavior> Behaviors => _behaviors;

    protected override void Activate(object content)
    {
        // single and multiple
        if (Views.IndexOf(content) == -1)
            Views.Insert(Journal.CurrentEntryIndex, content);

        // multiple
        if (ActiveViews.IndexOf(content) == -1)
            ActiveViews.Insert(Journal.CurrentEntryIndex, content);

        base.Activate(content);
    }

    public virtual void Attach(INavigationBehavior behavior)
    {
        if (behavior is null)
            throw new ArgumentNullException(nameof(behavior));

        behavior.NavigationService = this;
        behavior.Attach();
        _behaviors.Add(behavior);
    }

    public void Remove(object content)
    {
        base.Remove(content, r =>
        {
            // multiple
            ActiveViews.Remove(content);

            // single
            Views.Remove(content);
        });
    }

    public override void Clear()
    {
        Views.Clear();
        ActiveViews.Clear();
        base.Clear();
    }
}

public class SyncStackPanelBehavior : NavigationBehavior<StackPanel>
{
    protected CustomNavigationService CustomNavigationService => NavigationService as CustomNavigationService;

    protected override void OnAttach()
    {

        CustomNavigationService.Views.CollectionChanged += (s, e) =>
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    {
                        foreach (var item in e.NewItems)
                        {
                            if (item is FrameworkElement element)
                            {
                                if (TargetElement.Children.IndexOf(element) == -1)
                                    TargetElement.Children.Add(EnsureNewView(element));
                            }
                        }
                    }
                    break;
            }
        };

    }

    protected virtual FrameworkElement EnsureNewView(object content)
    {
        if (content is FrameworkElement)
        {
            var newContent = ContainerLocator.Current.Resolve(content.GetType()) as FrameworkElement;
            // if no AutoWireViewModel attached property on view or dataContext not defined at construction
            MvvmUtils.TryAutoWireViewModel(newContent);
            return newContent;
        }
        return null;
    }

    protected override void OnDetach()
    { }
}

// Example: custom navigation journal: insert each entry at beginning
public class CustomJournal : NavigationJournal
{
    protected override int GetNewEntryPosition() => 0;

    protected override void RemoveEntries(int startIndex)
    {
        while (EntriesInternal.Count < startIndex)
            EntriesInternal.RemoveAt(startIndex);
    }
}

Binding: Example ListBox Multiple

<ListBox ItemsSource="{Binding NavigationService.Views}"
            SelectionMode="Multiple"
            SelectedItem="{Binding NavigationService.Content}"
            Grid.Row="1">
    <ml:Interaction.Behaviors>
        <ml:SelectorSelectedItemsSyncBehavior ActiveItems="{Binding NavigationService.ActiveViews}" />
    </ml:Interaction.Behaviors>
</ListBox>

Inject the navigation service in MainWindow to add the behavior

public partial class MainWindow : Window
{
    public MainWindow(ICustomNavigationService navigationService)
    {
        InitializeComponent();

        navigationService.Attach(new SyncStackPanelBehavior { AssociatedObject = Panel });
    }
}

Multiple Navigation Services

Suggestion: Create a class with all Navigation services used

public interface IApplicationNavigationServices
{
    INavigationService Child { get; }
    INavigationService Main { get; }
}

public class ApplicationNavigationServices : IApplicationNavigationServices
{
    private INavigationService _main;
    public INavigationService Main
    {
        get
        {
            if (_main == null)
                _main = new NavigationService();
            return _main;
        }
    }

    private INavigationService _child;
    public INavigationService Child
    {
        get
        {
            if (_child == null)
                _child = new NavigationService();
            return _child;
        }
    }

    // etc.
}

... register with the container

Container.RegisterSingleton<IApplicationNavigationServices,ApplicationNavigationServices>();

... inject the ApplicationNavigationServices in ViewModels

Showing multiple shells

Create a ShellService and an interface ISupportNavigationService to set the NavigationService used by the ShellViewModel

public interface ISupportNavigationService
{
    INavigationService NavigationService { get; set; }
}

public interface IShellService
{
    void ShowShell();
}

public class ShellService : IShellService
{
    public virtual void ShowShell()
    {
        var shell = CreateShell();

        TryAutoWireViewModel(shell);
        TrySetNavigationService(shell);

        ShowShell(shell);
    }

    protected virtual Window CreateShell() => ContainerLocator.Current.Resolve<Shell>();

    protected virtual void TryAutoWireViewModel(FrameworkElement element) => MvvmUtils.TryAutoWireViewModel(element);

    protected virtual void TrySetNavigationService(object content)
        => MvvmUtils.ViewAndViewModelAction<ISupportNavigationService>(content, isns => isns.NavigationService = CreateScopedNavigationService());

    // for multiple navigation services: provide a new instance of a class with all navigation services
    protected virtual INavigationService CreateScopedNavigationService() => new NavigationService();

    protected virtual void ShowShell(Window shell) => shell.Show();
}

ShellViewModel sample

public class ShellViewModel : ISupportNavigationService, ISupportLoaded
{
    public ShellViewModel(IShellService shellService)
    {
        _shellService = shellService;
        Commands = new NavigationServiceCommands();
    }

    public INavigationService NavigationService { get; set; }
    public NavigationServiceCommands Commands { get; }

    private DelegateCommand _showShellCommand;
    private readonly IShellService _shellService;

    public DelegateCommand ShowShellCommand =>
        _showShellCommand ?? (_showShellCommand = new DelegateCommand(ExecuteShowShellCommand));

    private void ExecuteShowShellCommand()
    {
        _shellService.ShowShell();
    }

    public void OnLoaded()
    {
        Commands.Initialize(NavigationService);
    }
}

Data

ListCollectionViewEx

Is a ListCollectionView with shortcuts and usefull methods

public class ListCollectionViewSampleViewModel
{
    public ListCollectionViewSampleViewModel()
    {
        var users = new List<User>
        {
            new User { Id = 1, FirstName = "First.1", LastName ="Last.1", Age = 30, Role = UserRole.Admin },
            new User { Id = 2, FirstName = "First.2", LastName ="Last.2", Age = 40, Role = UserRole.User },
            new User { Id = 3, FirstName = "First.3", LastName ="Last.3", Age = 50, Role = UserRole.SuperAdmin },
            // ...
        };
        View = new ListCollectionViewEx<User>(users);

        Commands = new ListCollectionViewCommands(View)
        {
            Delete = Delete,
            Save = Save,
            Sort = new Action<string, ListSortDirection>((p, d) => View.Sort(p, d)),
            Filter = Filter
        };
    }

    public ListCollectionViewEx<User> View { get; }
    public ListCollectionViewCommands Commands { get; }

    private void Filter(string args)
    {
        if (string.IsNullOrWhiteSpace(args))
            View.ClearFilter();
        else
        {
            var splits = SplitText(args);
            if (splits.Length > 0)
            {
                var compositeFilter = new CompositeFilter<User>(LogicalOperator.Or);
                foreach (var split in splits)
                {
                    compositeFilter.AddFilter(new PropertyFilter<User>("FirstName", PredicateOperator.Contains, split));
                    compositeFilter.AddFilter(new PropertyFilter<User>("LastName", PredicateOperator.Contains, split));
                }
                View.Filter = compositeFilter.Filter;
            }
        }
    }

    private string[] SplitText(string text) => text.Split(' ', ',');

    private void Save()
    {
        try
        {
            if (View.IsAddingNew)
            {
                var current = View.CurrentAddItem as User;
                current.ValidateAll();
                if (!current.HasErrors)
                {
                    // save to db...
                    View.CommitNew();
                }
            }
            else if (View.IsEditingItem)
            {
                var current = View.CurrentEditItem as User;
                current.ValidateAll();
                if (!current.HasErrors)
                {
                    // save to db ..
                    View.CommitEdit();
                }
            }
        }
        catch (Exception ex)
        { }
    }

    private void Delete()
    {
        var current = View.Current;
        if (MessageBox.Show($"Delete {current.FirstName}?", "Confirmation", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
        {
            try
            {
                // remove from db ...
                View.Remove(current);
                // eventAggregator.GetEvent<NotificationMessageEvent>().Publish($"{name} removed!");
            }
            catch (Exception ex)
            {
                MessageBox.Show($"A problem occured:{ex.Message}");
            }
        }
    }
}

public class User : Validatable, IEditableObject
{
    private User oldUser = null;

    public void BeginEdit()
    {
        oldUser = new User
        {
            Id = Id,
            FirstName = FirstName,
            LastName = LastName,
            ImagePath = ImagePath,
            Age = Age,
            Role = Role
        };
    }

    public void CancelEdit()
    {
        if (oldUser == null)
            throw new InvalidOperationException();

        FirstName = oldUser.FirstName;
        LastName = oldUser.LastName;
        ImagePath = oldUser.ImagePath;
        Age = oldUser.age;
        Role = oldUser.role;
    }

    public void EndEdit()
    {
        oldUser = null;
    }

    private string firstName;
    [Required]
    public string FirstName
    {
        get { return firstName; }
        set { SetProperty(ref firstName, value); }
    }

    // etc.
}

PagedList (IEnumerable T extension method)

int pageNumber = 1;
int pageSize = 50;
var pagedList = Users.ToPagedList<User>(pageNumber, pageSize);

Binding

<ListBox ItemsSource="{Binding PagedList}"></ListBox>

PagedSource

public class PagedSourceViewModel : BindableBase
{
    public PagedSourceViewModel()
    {
        Users = new ObservableCollection<User>();
        PopulateUsers();
        PagedSource = new PagedSource(Users);
        Commands = new PagedSourceCommands(PagedSource);

        AddNewItemCommand = new DelegateCommand(AddItem);
    }

    public ObservableCollection<User> Users { get; }
    public PagedSource PagedSource { get; }
    public PagedSourceCommands Commands { get; }
    public ICommand AddNewItemCommand { get; }

    private void AddItem()
    {
        var user = new User();
        PagedSource.View.AddNewItem(user);
        PagedSource.MoveToLastPage();
        PagedSource.PageView.MoveCurrentToLast();
    }

    // etc.
}

Binding

<!-- commands -->
<Button Command="{Binding Commands.MoveCurrentToFirstCommand}" >
    <controls:MaterialDesignIcon Icon="SkipPrevious" Brush="#2980b9"/>
</Button>

<!-- currentItem -->
<ContentControl Content="{Binding PagedSource.PageView.CurrentItem, Mode=OneWay}"></ContentControl>

It's possible to create a DataPager and the methods of the PagedSource

  • MoveToFirstPage
  • MoveToPreviousPage
  • MoveToNextPage
  • MoveToLastPage
  • MoveToPage

Controls

AnimatingContentControl

Content Control that allows to animate on content change.

2 Storyboards :

  • EntranceAnimation
  • ExitAnimation
  • Simultaneous (boolean) allows to play simultaneously the animations.
  • CanAnimateOnLoad: allows to cancel animation on load
<ml:AnimatingContentControl ml:NavigationManager.SourceName="Main">
    <ml:AnimatingContentControl.EntranceAnimation>
        <Storyboard>
            <!-- Target "CurrentContentPresenter"  -->
            <DoubleAnimation Storyboard.TargetName="CurrentContentPresenter"
                             Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"
                             From="400" To="0" Duration="0:0:0.4"  />
        </Storyboard>
    </ml:AnimatingContentControl.EntranceAnimation>
    <ml:AnimatingContentControl.ExitAnimation>
        <Storyboard>
            <!-- Target "CurrentContentPresenter" or with Simultaneous "PreviousContentPresenter" -->
            <DoubleAnimation Storyboard.TargetName="CurrentContentPresenter"
                             Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"
                             From="0" To="400" Duration="0:0:0.4"  />
        </Storyboard>
    </ml:AnimatingContentControl.ExitAnimation>
</ml:AnimatingContentControl>

Or Simultaneous

  <ml:AnimatingContentControl Content="{Binding Navigation.Current}"
                                    Simultaneous="True"
                                    IsCancelled="{Binding IsCancelled}">
    <ml:AnimatingContentControl.EntranceAnimation>
        <Storyboard>
            <DoubleAnimation  Storyboard.TargetName="CurrentContentPresenter"
                                Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"
                                From="{Binding ElementName=ThisControl,Path=ActualWidth,FallbackValue=400}" To="0"
                                Duration="{Binding ElementName=DuractionComboBox,Path=SelectedItem}">
                <DoubleAnimation.EasingFunction>
                    <SineEase EasingMode="EaseInOut" />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>
    </ml:AnimatingContentControl.EntranceAnimation>
    <ml:AnimatingContentControl.ExitAnimation>
        <Storyboard>
            <DoubleAnimation  Storyboard.TargetName="PreviousContentPresenter"
                                Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"
                                From="0" To="{Binding ElementName=ThisControl,Path=ActualWidth,FallbackValue=400}"
                                Duration="{Binding ElementName=DuractionComboBox,Path=SelectedItem}">
                <DoubleAnimation.EasingFunction>
                    <SineEase EasingMode="EaseInOut" />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>
    </ml:AnimatingContentControl.ExitAnimation>
</ml:AnimatingContentControl>

Other sample: animations in resources

<UserControl.Resources>
    <Storyboard x:Key="EntranceAnimation1">
        <DoubleAnimation Storyboard.TargetName="CurrentContentPresenter"
                         Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"
                         From="400" To="0" Duration="0:0:0.4"  />
    </Storyboard>

    <Storyboard x:Key="ExitAnimation1">
        <DoubleAnimation Storyboard.TargetName="CurrentContentPresenter"
                         Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"
                         From="0" To="-400" Duration="0:0:0.4"  />
    </Storyboard>
</UserControl.Resources>
<ml:AnimatingContentControl Content="{Binding Navigation.Current}"
                                 EntranceAnimation="{StaticResource EntranceAnimation1}"
                                 ExitAnimation="{StaticResource ExitAnimation1}">
</ml:AnimatingContentControl>

Other sample: Change Animations dynamically and controlling when the animation is played with "CanAnimate". For a Schedule view for example.

<ml:AnimatingContentControl  Content="{Binding Navigation.Current}"
                                  CanAnimate="{Binding CanAnimate, Mode=OneWay}"
                                  CanAnimateOnLoad="False"
                                  EntranceAnimation="{Binding EntranceAnimation, Mode=OneWay}"
                                  ExitAnimation="{Binding ExitAnimation, Mode=OneWay}">

    <ml:Interaction.Triggers>
        <ml:EventTrigger EventName="AnimationCompleted">
            <ml:CallMethodAction TargetObject="{Binding}" MethodName="CompleteChangingScheduleMode"/>
        </ml:EventTrigger>
    </ml:Interaction.Triggers>

</ml:AnimatingContentControl>

TransitioningContentControl

Allows to play a transition on loaded.

2 Storyboards:

  • EntranceTransition: played when control loaded (or explicitly with "DoEnter")
  • ExitTransition: played explicitly with "DoLeave" or IsLeaving dependency property (for example played when the user click on a tab close button)

Other methods:

  • CancelTransition
  • Reset: reset the render transform property and opacity + cancel transition
<ml:TransitioningContentControl x:Name="TransitioningContentControl1" Margin="0,20">
        <ml:TransitioningContentControl.EntranceTransition>
            <Storyboard>
                <DoubleAnimation Storyboard.TargetName="ContentPresenter"
                                    Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"
                                    From="0" To="1" Duration="0:0:0.6">
                    <DoubleAnimation.EasingFunction>
                        <ExponentialEase EasingMode="EaseInOut"/>
                    </DoubleAnimation.EasingFunction>
                </DoubleAnimation>
            </Storyboard>
        </ml:TransitioningContentControl.EntranceTransition>
        <ml:TransitioningContentControl.ExitTransition>
            <Storyboard>
                <DoubleAnimation Storyboard.TargetName="ContentPresenter"
                                    Storyboard.TargetProperty="(UIElement.Opacity)"
                                    From="1" To="0" Duration="0:0:2"/>
            </Storyboard>
</ml:TransitioningContentControl.ExitTransition>

TransitioningItemsControl

ItemsControl that allows to animate on item insertion and deletion.

The "ControlledAnimation" avoid to set the target and the target property of the storyboard. The TargetPropertyType is a shortcut. But it's possible to target explicitly the target property of the storyboard with "TargetProperty" dependency property.

<ml:TransitioningItemsControl ItemsSource="{Binding MyItems}"
                                   TransitionClearHandling="Transition"
                                   IsCancelled="{Binding IsCancelled}">
    <ml:TransitioningItemsControl.EntranceAnimation>
        <ml:ParallelAnimation>

            <ml:ControlledAnimation TargetPropertyType="TranslateX">
                <DoubleAnimation From="200" To="0"  Duration="0:0:2"/>
            </ml:ControlledAnimation>

        </ml:ParallelAnimation>
    </ml:TransitioningItemsControl.EntranceAnimation>

    <ml:TransitioningItemsControl.ExitAnimation>
        <ml:ParallelAnimation>
            <ml:ControlledAnimation TargetPropertyType="TranslateX">
                <DoubleAnimation From="0" To="200" Duration="0:0:2"/>
            </ml:ControlledAnimation>
        </ml:ParallelAnimation>
    </ml:TransitioningItemsControl.ExitAnimation>
</ml:TransitioningItemsControl>

It's a control that allows to use navigation service méthods (like a Frame).

Example with mahApps.Metro, sets the content of the HamburgerMenu

public partial class Shell
{
    public Shell(IConfigurableNavigationService navigationService)
    {
        InitializeComponent();

        var navigatableContentControl = new NavigatableContentControl();
        navigatableContentControl.NavigationService = navigationService;
        HamburgerMenuControl.Content = navigatableContentControl;
    }
}

Example 2 xaml

<!-- xmlns:ml="http://mvvmlib.com/" -->
<ml:NavigatableContentControl x:Name="NavigatableContentControl"/>

Interactivity

Uses Microsoft.Xaml.Behaviors.Wpf

Namespaces:

xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:ml="http://mvvmlib.com/"

CallMethodAction with parameter

<Button Content="CallMethodAction Sample">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <ml:CallMethodAction TargetObject="{Binding}" MethodName="MyMethod" Parameter="MvvmLib!"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

Create a custom TriggerAction

Example: for closing TabItem

public class CloseTabAction : TriggerAction<Button>
{
    protected override void Invoke(object parameter)
    {
        var args = parameter as RoutedEventArgs;
        if (args == null)
            return;

        var tabItem = XamlTreeHelper.FindParent<TabItem>((DependencyObject)args.OriginalSource);
        if (tabItem == null)
            return;

        var tabControl = XamlTreeHelper.FindParent<TabControl>(tabItem);
        if (tabControl == null)
            return;

        ((MainWindowViewModel)tabControl.DataContext).NavigationService.Remove(tabItem.Content);
    }
}

public class XamlTreeHelper
{
    public static T FindParent<T>(DependencyObject child) where T : DependencyObject
    {
        DependencyObject parentObject = VisualTreeHelper.GetParent(child);

        if (parentObject == null)
            return null;

        var parent = parentObject as T;
        if (parent != null)
            return parent;

        return FindParent<T>(parentObject);
    }
}

Add the trigger to the TabItem implicit Style

<Style TargetType="TabItem">
    <Setter Property="Header" Value="{Binding DataContext.Name}"/>
    <Setter Property="HeaderTemplate">
        <Setter.Value>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <ContentControl Content="{Binding}" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0,0,7,0" />
                    <Button Content="X"
                            HorizontalAlignment="Right" Height="20" Width="20" Padding="0"
                            Grid.Column="1">
                        <ml:Interaction.Triggers>
                            <ml:EventTrigger EventName="Click">
                                <local:CloseTabAction />
                            </ml:EventTrigger>
                        </ml:Interaction.Triggers>
                    </Button>
                </Grid>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

And the TabControl: ItemsSource binded to the Views of a Custom NavigationService

<TabControl ItemsSource="{Binding NavigationService.Views}"></TabControl>

Behaviors

EventToCommandBehavior

<Button Content="EventToCommandBehavior Sample">
    <i:Interaction.Behaviors>
        <ml:EventToCommandBehavior EventName="Click" Command="{Binding MessageCommand}" CommandParameter="World"/>
    </i:Interaction.Behaviors>
</Button>

SelectorSelectedItemsSyncBehavior

<ListBox ItemsSource="{Binding Users}"
            SelectionMode="Multiple"
            SelectedItem="{Binding SelectedUser}">
    <i:Interaction.Behaviors>
        <ml:SelectorSelectedItemsSyncBehavior ActiveItems="{Binding SelectedUsers}"/>
    </i:Interaction.Behaviors>
</ListBox>

TreeViewSelectedItemChangedBehavior

 <TreeView ItemsSource="{Binding Families}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type viewModels:Family}" ItemsSource="{Binding Members}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" />
            </StackPanel>
        </HierarchicalDataTemplate>
        <DataTemplate DataType="{x:Type viewModels:FamilyMember}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" />
            </StackPanel>
        </DataTemplate>
    </TreeView.Resources>
    <i:Interaction.Behaviors>
        <ml:TreeViewSelectedItemChangedBehavior
            ExpandSelected="True"
            SelectedItem="{Binding ActiveFamilyOrMember}" />
    </i:Interaction.Behaviors>
</TreeView>

BindingProxy

Example:

<DataGrid x:Name="DataGrid1" ItemsSource="{Binding CollectionView}" AutoGenerateColumns="False" IsReadOnly="True">
    <DataGrid.Resources>
        <!-- 1. Adds the Proxy in control or window resources-->
        <ml:BindingProxy x:Key="Proxy"  Data="{Binding}"/>
    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding FirstName}" Width="*">
            <DataGridTextColumn.Header>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition Width="Auto" />
                    </Grid.ColumnDefinitions>

                    <TextBlock Text="Name" />

                    <local:DropDownButton Grid.Column="1">
                        <local:DropDownButton.DropDownContent>
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition />
                                    <RowDefinition Height="Auto"/>
                                </Grid.RowDefinitions>

                                <!-- code -->

                                <StackPanel Orientation="Horizontal" Grid.Row="1">
                                    <!-- 2. Use the Proxy as Source and bind with The Data dependency property -->
                                    <Button Content="Filter" Command="{Binding Data.FilterFirstNameCommand, Source={StaticResource Proxy}}" />
                                </StackPanel>
                            </Grid>
                        </local:DropDownButton.DropDownContent>
                    </local:DropDownButton>
                </Grid>
            </DataGridTextColumn.Header>
        </DataGridTextColumn>

        <!-- other columns -->
    </DataGrid.Columns>
</DataGrid>