MVVM Design Pattern


Introduction

A few aspects of MVVM hit the spot with me and I tried to see what it will take to build the simplest MVVM. I started with the very basic concept of creating, binding and a few features as I needed them. The following code is simple, extensible, and provides an easier way to follow some MVVM concepts. My approach is that the MVVM design pattern is a set of recommendations not a set of rules.

Scaffolding

This project provides the frame and building blocks for the MVVM pattern, and a few other basic tools. Most of the Kung-Fu takes place in the "CreateAndInject" file which contains a class with the same name. This class exposes one static method named "CreateAndBindViewAndViewModel" which instantiates the objects, binds them and finally injects them where I want (details in the next 3 sections).

The Scaffolding project also supports reading configuration data; it includes the basic Interfaces of a few events, delegates and support data classes, and initial commands (primitive and will get extended on the next installment).

I'll start with the View-ViewModel (V-VM) definition which is contained in "ModelViewRelation" class (in a file of same name). Here I specify the names of the 2 types (using string) and a display name that may appear if needed. For good measure I also allow this definition to include an object (will come in handy when I try to inject data into a V-VM).

The creation of the VM and the V is done by reflection and for that I need (beside the type names) the assembly in which the V and VM types are located and the namespaces for the types. The "ModelViewRelation" contains default values for those strings; these values can also be loaded programmatically at run time, or configured in the 'ServiceReferences.ClientConfig' file:

  <configuration>
  <appSettings>
                <add key="AssemblyVersion" value="1.0.0.0" />
                <add key="AssemblyName" value="HomeGrownMVVM" />
                <add key="ViewModelPath" value="HomeGrownMVVM.ViewModel" />
                <add key="ViewPath" value="HomeGrownMVVM.View" />
                <add key="ModelSuffix" value="Model" />
                <add key="QueryRandomizer" value="3" />
  </appSettings>

Using this class is simple:

var mvr = new ModelViewRelation("Classic CD's", "ClassicView");

In the above line I implicitly say that the VM type is "ClassicViewModel". If you I want to bind the "ClassicView" to the "RockViewModel" I have to do the following:

var mvr = new ModelViewRelation("Classic CD's""ClassicView"", "RockViewModel");

View and ViewModel Creating

As mentioned the heavy lifting is done by a static method name "CreateAndBindViewAndViewModel" which provides the following MVVM (and so MVVM) activities:

  • Create the View and the ViewModel - by CreateViewModelAndView()
     
  • Bind (and wire up) the View to the ViewModel - by WireAndBind ()

  • Inject the View into its host - by delegate: injectView
     
This method calls in sequence 3 private methods each covers one of the above steps.

The static method "CreateAndBindViewAndViewModel" within the "CreateAndInject" class takes 4 parameters and returns true if all has succeeded.

        public static bool CreateAndBindViewAndViewModel(
                                                                ModelViewRelation mvRelation, //defines the View and ViewModel types
                                                                InjectViewDelegate injectView, //call this delegate to inject the view
                                                                UIElement host= null, //optional container for the view (used by the injector)
                                                                IBaseViewModel substitute = null) //optional (existing) object to use as the ViewModel
{
                UIElement view;
                //instantiate the objects
                object viewModel = CreateViewModelAndView(mvRelation, substitute, out view);
                //wire the objects
                WireAndBind(mvRelation, view, viewModel);
                if (view == null)
                {
                                MessageBox.Show("Fail to launch " + mvRelation.ToString());
                                return false;
                }
                return injectView(mvRelation, view, host) != null; //inject the View
}

No magic here…

Moving on - instantiate the VM and V

        private static object CreateViewModelAndView( //returns the created or existing ViewModel
        ModelViewRelation mvRelation, //defines the View and ViewModel types
        IBaseViewModel substitute, //may be used as a ViewModel
        out UIElement view) //returns the created View
        {
            view = null;
            Assembly assembly = null;
            object viewModel = null;

            if (mvRelation.IsValid == true)
            {//for security, Silverlight requires the long form of assembly name
                var secureName = string.Format(
                "{0}, Version={1}, Culture=neutral, PublicKeyToken=null",
                mvRelation.AssemblyName,
                mvRelation.AssemblyVersion);
                try
                {//load the assembly
                    assembly = Assembly.Load(secureName);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
                if (assembly != null)
                {//create a new VM or use the passed in substitute?
                    viewModel = mvRelation.ViewModelName.EndsWith(".this") ?
                    substitute : //the new View will bind to this (passed-in) existing ViewModel
                    CreateTheObject(assembly, mvRelation.ViewModelName); //create a new ViewModel
                    if (viewModel != null)
                    {// VM is here, create the View from a given fully qualified type name
                        view = CreateTheObject(assembly, mvRelation.ViewName) as UIElement;
                    }
                }
            }
            return viewModel;
        }

Again, no miracles here, but few points worth mentioning:

If this file "CreateAndInject" is included within the same project of the Views and ViewModel's, a simpler call to Assembly.GetExecutingAssembly () can be used.

If the call to this class is only done from the same project of the Views and ViewModel's, a simpler call to Assembly. GetCallingAssembly () can be used, but unlike C++, C# doesn't have inline keyword; therefore you must incorporate the code of this method into the main public method.

The code I wrote is more general-purpose with the caveat that when the assembly that contains the V and VM types changes its name or version – update to the configuration file is needed. If other changes are expected (Culture, etc.) - move the rest of the 'full name' parameters to the configuration file.

A built-in allowance for 'recycling' a VM allows rewiring a new View to an existing ViewModel (see example in the "InformingView_NotifyEvent" method in "BaseViewMode"). Right now I do not unbind the existing view when I use that facility since I never needed it – but it can be done. Extending the "ModelViewRelation" class is extremely simple and so should the unbinding be.

The actual instantiation happens in the "CreateTheObject" method:

        private static object CreateTheObject(Assembly assembly, string objectName)
        {
            object obj = null;
            var objectType = assembly.GetType(objectName);
            if (objectType != null)
            {//found the type in the assembly, instantiate the object
                obj = Activator.CreateInstance(objectType);
            }
            return obj;
        }

View-ViewModel Binding

This is the second step which is carried out by the "WireAndBind" method.

        private static void WireAndBind(
        ModelViewRelation mvRelation, //defines the View and ViewModel types
        UIElement view, //the View that needs binding to VM
        object viewModel) //the ViewModel that will be bound to the V
        {
            if (view != null)
            {
                if (viewModel is IBaseViewModel)
                {
                    if (mvRelation.Anything != null)
                    {//inject Data into the ViewModel, if so 'requested'
                        (viewModel as IBaseViewModel).ModelData = mvRelation.Anything;
                    }
                    if (view is IDirectData)
                    {//pass the VM a reference of the V for data injecting rather than binding
                        (viewModel as IBaseViewModel).DirectDataToView = view as IDirectData;
                    }
                    //call LoadData() method on the VM
                    (viewModel as IBaseViewModel).LoadData();
                }
                if (view is FrameworkElement)
                {//data-bind the View to the ViewModel
                    (view as FrameworkElement).DataContext = viewModel;
                }
                if ((view is IInforming) && (viewModel is IBaseViewModel))
                {//allow the View to fire custom NotifyEvent
                    (viewModel as IBaseViewModel).Subscribe(view as IInforming);
                }
                if ((view is ILaunchChild) && (viewModel is IBaseViewModel))
                {//allow The View to fire LaunchChildEvent
                    (viewModel as IBaseViewModel).Subscribe(view as ILaunchChild);
                }
            }
        }

Still no miracles (maybe one or two MVVM sins though...), this method checks which interfaces were declared and based on that performs certain actions. By order of appearance:

  1. Inject data to the VM utilizing the ModelData variable which is part of the IBaseViewModel Interface and lives in the BaseViewModel class. This action will take place only if the user set the optional parameter 'Anything' in the ModelViewRelation (see example in the "InformingView_NotifyEvent" method in "BaseViewMode"). It can be handy for writing Generic ViewModel. 
     
  2. Pass a reference of the View to the ViewModel in its DirectDataToView variable (I know, literal MVVM disciples shake their heads). This crime/sin will take place only if the View implements the IDirectData Interface (disciples: do not declare this interface). When the View does expose such Interface the ViewModel can push to the view a List< IModelViewData >. But more than that if the ViewModel up-casts the DirectDataToView and get a hold of the View. There is hardly ever a need to use this interface.
     
  3. Call the Load Data method within the ViewModel to allow the VM doing any work needed before the View gets a 'glimpse' of the data which will get bound as the next step. LoadData is part of the IBaseViewModel Interface and the virtual method lives in the BaseViewModel and obviously can be overridden by any ViewModel. The constructor of BaseViewModel calls its own Initialize to carry out any activities that need to take place before the LoadData can be called (e.g. prepare the WCF proxy).
     
  4. Bind the View to the ViewModel, provided that the View is a FrameworkElement.
     
  5. Register the ViewModel to receive custom events of type NotifyEvent. This subscription will happen only if the View implements the IInforming Interface. This is not an MVVM standard, nevertheless it allows me to quickly do some testing, overcome the lack of commands, and as rule don't implement the interface if you don't want this channel of communication opened.
     
  6. Register the ViewModel to receive custom events of type LaunchChildEvent. This subscription will happen only if the View implements the ILaunchChild Interface. Same comments as before…
'Simple' View Injecting

This part is a little trickier only because I do not know where the new view is to be injected and how to do it, so here I lean heavily on the caller and keep my life simple: The caller must supply me with an injection delegate and I will call it. The injection delegate's signature takes 3 parameters and returns one:

        public delegate UIElement InjectViewDelegate(//return the created view or null if failed
        ModelViewRelation mvRelation, //defines the View and ViewModel types
        UIElement boundView, //the created View
        UIElement host); //hosts the view

To clarify the injector delegate it is best to look at an example. For that I need to spend few words on the demo.

TabControl with its TabItems is a very nice way to quickly switch between different files (visual studio) or switch between different views. The demo launches views into a TabControl while each time it creates a new TabItem to host the View.

For the injector example I will look at the view that is created by the side menu. Below is the Welcome page of the demo.

When clicking on any link in the side menu a view will be injected into a new tab page which is added into a tab-control as shown in the next picture:

It is not hard to guess that the click on the side menu send a populated "ModelViewRelation" class into the "CreateAndInject" method. Here is the code snippet for that:

        private void MenuItemSelected(object obj)
        {
            var mvRelation = obj as ModelViewRelation;
            if (mvRelation != null)
            {
                CreateAndInject.CreateAndBindViewAndViewModel(mvRelation, InjectView, null);
            }
        }

As can be seen, I receive here the populated mvRelation, I pass in the InjectView method as a delegate, and I leave the host as a null (mainly since the host doesn't exist yet). The mvRelation get populated when I construct the Side Menu. This "CreateAndInject" method will instantiate the MV and the V, bind them and call the injector which is coming up soon.
The injector usually is simpler than this one – this injector is a little longer because it does some more than just injecting a view. Much simpler examples will follow later. Here goes:

        private TabItem InjectView(ModelViewRelation mvRelation, UIElement view, UIElement host)
        {
            var tabPage = CreateTabPage(mvRelation.DisplayName, view);
            if (tabPage != null)
            { //this collection is bound to the TabControl, hence adding an item adds a TabItem
                TabPagesCollection.Add(tabPage);
                TopTabPage = tabPage; //TopTabPage is bound to the SelectedItem
            }
            UpdateViwingArea(); //Tab controls is visible if it contains items otherwise collapsed
            return tabPage;
        }
 
        private TabItem CreateTabPage(string header, UIElement view)
        {
            if (view == null)
            {
                return null;
            }
            //enhance the header: allow closing a TabItem, it displays V-VM name
            var hdr = new TabHeaderControl(header);
            hdr.CloseMeEvent += CloseTabItemEvent; //register for the Close Event
            var tab = new TabItem //create a new TabItem
            {
                Content = view, //INJECT THE VIEW !!!
                Header = hdr, //inject the fancy header
                HorizontalAlignment = HorizontalAlignment.Stretch,
                HorizontalContentAlignment = HorizontalAlignment.Stretch
            };
            hdr.Tag = tab; //let the header have a reference to its own tab (for closing)
            return tab;
        }

The injector starts with calling "CreateTabPage" to create a new TabItem. The code in "CreateTabPage" creates a user control (TabHeaderControl) to provide means to close the TabItem. Then it goes to create the TabItem itself and inject the view – as expected - into the item's content. Next it places the header control into the header. Lastly it stores a reference to the new tab in the header control so it knows which one to close.

The injector adds the TabItem that was created to the TabPagesCollection. This collection is bound to the ItemSource of the TabControl; hence a new TabItem is added to the TabControl. This TabItem includes the injected View. Next the injector sets TopTabPage to the new TabItem, the TopTabPage is bound to the SelectedItem of the TabControl therefore the TabItem I just added will be on top of all the other. Lastly it updates the visibility of TabControl based on the number of items it has (0 = collapsed otherwise visible).

View into View Injecting

This demo pretends to be a tool to query a database. So I have created a V-VM that collects, manipulates, and displays the Classic CD's. The V in this case contains a DataGrid of all CD's, some status and summary text and the ability to click on any row in the grid to see a details view of the specific CD.

Once the above V-VM was working I wanted to create a new V-VM to allow the user to perform a query into the database. This new view should include means for the user to specify the query – at least a text box – and a submit button to run the query. Once he runs the query the results come back and it will be extremely helpful if I can use the same view I build earlier, but I do not want to lose the query portion of the view – I want to inject this first view into my new query view.

It is quite simple. I add a ContenPresenter to my query V it will be the host of the first V. Here is the XAML for that (in file ClassicQueryView.xaml)

<ContentPresenter Height="Auto"
Margin="7,7,2,2"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Grid.Row="1"
Loaded="ContentPresenter_Loaded"
Tag="[ClassicView]"
Visibility="{Binding ShowQueryResults}"

</ContentPresenter>

The "Visibility" is bound so that it is visible only if a query was submitted.

The Loaded event will trigger the creation, binding, and injection of the requested View-ViewModel.

The requested V-VM is specified using the Tag. The home grown format is [View][ViewModel] or [View] the last format is used when the current ViewModel is to be bound to the new View.

When the Loaded event is handled I pick up the info from the Tag, populate a V-VM Relation class, call the faithful "CreateAndInject", and sit back and watch the view being injected.

I'll start from the end and look at the injector-delegate:

        private UIElement InjectTheView(ModelViewRelation mvRelation, UIElement view, UIElement host)
        {
            var presenter = host as ContentPresenter;
            if ((presenter != null) && (view != null))
            {
                presenter.Content = view;
            }
            return view;
        }

It is hard to get simpler code than that. You would expect to find this injector in the ViewModel of the Query View, but I pushed it back to the BaseViewModel only because there are quite enough times I want to inject a view into a view and this way if I always use ContentPreseter I do not have to write any code – just add it to the XAML and handle the event and that is it.

Back to the start – the loaded event was fired; I handle it in the code behind of the Query View (just because I do not have commands yet). Below is the complete code-behind file, include the hander of the Loaded event (ClassicQueryView.xaml.cs):

using System.Windows;
using System.Windows.Controls;
using VvmScaffolding;
 
namespace HomeGrownMVVM.View
{
    public partial class ClassicQueryView : UserControl, IInforming
    {
        public ClassicQueryView()
        {
            InitializeComponent();
        }

        private void ContentPresenter_Loaded(object sender, RoutedEventArgs e)
        {
            if (NotifyEvent != null)
            {
                NotifyEvent(sender, new GenericEventArgs(sender));
            }
        }

        public event NotifyEventHandler NotifyEvent;
    }
}

The "ClassicQueryView" class implements the Informing interface hence if it raises the NotifyEvent and the bound VM will handle it. The event handler takes the sender and places it in a custom GenericEventArg.

Next step is to look at handling this event, which is done in the ViewModel; again I push the handler to the BaseViewModel so I do not have to worry about it anymore. From BaseViewModel:

        protected virtual void InformingView_NotifyEvent(object sender, GenericEventArgs e)
        {
            var presenter = e.Camel as FrameworkElement;
            if (presenter != null)
            { //it is a FrameElement which means it contains Tag
                var names = presenter.Tag as string; //read the Tag
                if (string.IsNullOrEmpty(names) == false)
                { //it contains a string lets process it
                    e.Handled = true; //tell overriding handlers that i handled the event.
                    var separators = new[] { '[', ']', ' ' };
                    names = names.Trim(separators);
                    var saNames = names.Split(separators, StringSplitOptions.RemoveEmptyEntries);
                    ModelViewRelation mvr = null;
                    switch (saNames.Length)
                    {
                        case 1: //this VM will bo bound to the View
                            mvr = new ModelViewRelation("Dynamic View", saNames[0], "this");
                            break;
                        case 2: //create a new V and VM
                            mvr = new ModelViewRelation("Dynamic View", saNames[0], saNames[1]);
                            break;
                    }
                    CreateAndInject.CreateAndBindViewAndViewModel(mvr, InjectTheView, presenter, this);
                }
            }
        }

Since this handler is in the base class, every class that handles that event overrides this handler. The overriding handler must first call the base handler and continue only if the base hasn't handled it. After extracting the name/s from the tag the code populates the ModelViewRelation. If only one name found it insert the string 'this' as the name of the VM - that will cause the "CreateAndInject" to skip instantiating a VM. The call to "CreateAndInject" passes the newly created ModelViewRelation, an inject-delegate (the one we looked at earlier), the presenter (the sender of the Loaded event) as the host and itself as a substitute VM (in case substituting is needed).

In this case the VM for the new V is the existing "ClassicQueryViewModel". If I want the new view to be able to launch a child view (see next section), I need to copy the handler for that even from "ClassicViewModel" into the "ClassicQueryViewModel".

Launching Child View

In this demo when I click on a grid row, I want to get a details view of the CD in this row. This details view will be created using a ChildWindow. The View name is DetailsChildWindowView.xaml and the ViewModel follows the naming convention DetailsChildWindowViewModel.cs.

This is a case where the VM gets the data injected into it (indirectly) by the launching VM I'll use the ClassicViewModel as the launching one. Another difference is that this V-VM is hosted by no other view I was tempted to call it floating view but ChildWindow may be closer to the truth. The fact that this view has no host is not a problem really, since the Injector will know what to do.

Let me recap: I click on a row, SelectionChanged event is fired, event is handled by the code behind, launching a V-VM, inject the selected–row-details into the launched V-VM, and show the view.

Here is DataGrid portion of the XAML of the ClassicView.xaml:

<sdk:DataGrid AutoGenerateColumns="True"
HorizontalAlignment="Left"
Margin="5"
VerticalAlignment="Top"
Grid.Row="1"
ItemsSource="{Binding DataToPresent}"
SelectionChanged="DataGrid_SelectionChanged"
/>

Here is the handler of the SelectionChanged event in the code behind (ClassicView.xaml.cs)

        private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if ((NotifyEvent != null) && (e.AddedItems.Count > 0))
            {
                NotifyEvent(sender, new GenericEventArgs(e.AddedItems[0]));
            }
        }

Once it has determined there is a selected row, it passes the item (the class PresentingClassicData, which was bound through the XAML) into the GenericEventArgs and fires the NotifyEvent.

The NotifyEvent is handled by the VM - ClassicViewModel.

        protected override void InformingView_NotifyEvent(object sender, GenericEventArgs e)
        {
            base.InformingView_NotifyEvent(sender, e);
            if (e.Handled == true)
            { // base class handled this event
                return;
            }
            var r = e.Camel as PresntingClassicData;
            if (r != null)
            { //this is PresntingClassicData as expected, create ModelViewRelation
                var mvvmDef = new ModelViewRelation("Details", //name
                "DetailsChildWindowView", //View
                "DetailsChildWindowViewModel", //ViewModel
                r.Details); //Injected data
                PopUpView(this, new ModelViewRelationEventArgs(mvvmDef));
            }
            var dg = sender as DataGrid;
            if (dg != null)
            { //clear selection, so that clicking on same item again will create SelectionChange event
                dg.SelectedItems.Clear();
            }
        }

PopUpView is called to launch the view. The PopUpView is pushed into the BaseViewModel so every VM can use it and its injector:

        public void PopUpView(object sender, ModelViewRelationEventArgs e)
        {
            CreateAndInject.CreateAndBindViewAndViewModel(e.Holder, ShowDetails);
        }
        //The Injector
        public ChildWindow ShowDetails(ModelViewRelation mvRelation, UIElement view, UIElement host)
        {
            var cw = view as ChildWindow;
            if (cw != null)
            {
                cw.Show();
            }
            return cw;
        }

The PopUpView calls CreateAndBindViewAndViewModel. It passes to it an injector delegate - ShowDetails. The injector simply receives the view and makes sure it is ChildWindow class. Once it is a ChildWindow, the injector calls Show on it.

Obtaining Configuration Data

The "ConfigurationManager.cs" file provides means to obtain configuration data from ServiceReferences.ClientConfig. I used configuration data to pass configuration data to the application and also to create the initial Side Menu and its V-VM settings.

The "AppSetting" contains a static method "GetMenuItems" which goes through the configuration file and creates a list of 3 strings (Display Name, View Name, and ViewModel Name).

        static public List<ThreeSringHolder> GetMenuItems()
        {
            var list = new List<ThreeSringHolder>();
            var settings = new XmlReaderSettings();
            settings.XmlResolver = new XmlXapResolver();
            var reader = XmlReader.Create(configFile);
            reader.MoveToContent();
            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    if (reader.Name == "MenuItem")
                    {
                        string d = reader.GetAttribute("Name");
                        string v = reader.GetAttribute("View");
                        string vm = reader.GetAttribute("ViewModel");
                        var item = new ThreeSringHolder(d, v, vm);
                        list.Add(item);
                    }
                    else
                    {
                        if (list.Count > 0)
                        {
                            break;
                        }
                    }
                }
            }
            return list;
        }

Similar code in "GetAppSetting" retrieves value from a Key-Value entrees and another overriding method allows specifying a default value while retrieving the value.

The Side Menu

The Side Menu is created at the WelcomPage file. It a ListBox (of HyperlinkButton items) bound to a collection of ModelViewRelation objects.

        private void CreateMenuList()
        {
            MenuItemsList = new ObservableCollection<ModelViewRelation>
{
new ModelViewRelation("Classic CD's", "ClassicView"),
};
            var list = AppSetting.GetMenuItems();
            foreach (var item in list)
            {
                if ((string.IsNullOrEmpty(item.Name) == true) && (string.IsNullOrEmpty(item.View) == true))
                { //will create a menu separator line
                    MenuItemsList.Add(new ModelViewRelation("", ""));
                }
                else if (string.IsNullOrEmpty(item.ViewModel) == true)
                { //VM follows name convention

                    MenuItemsList.Add(new ModelViewRelation(item.Name, item.View));
                }
                else
                { //names for both V and VM are supplied
 
                    MenuItemsList.Add(new ModelViewRelation(item.Name, item.View, item.ViewModel));
                }
            }
        }

The first item is created from values in the code; next it calls "AppSetting.GetMenuItems" to obtain all the strings from the configuration files. Lastly it constructs "ModelViewRelation" from the list and adds all of them to the "MenuItemsList" collection.

This collection is bound to the ListBox that creates the Side Menu so that the Display name is shown to the user. Clicking on HyperlinkButton ends up (utilizing the Silverlight commanding) with a call to "MenuItemSelected":

        private void MenuItemSelected(object obj)
        {
            var mvRelation = obj as ModelViewRelation;
            if (mvRelation != null)
            {
                CreateAndInject.CreateAndBindViewAndViewModel(mvRelation, InjectView, null);
            }
        }

This method creates and binds the V-VM and injects it into a TabItem that is added to the TabControl.

Base ViewModel and its Interface

The base model implements the IBaseViewModel interface that supports the activities by the binder:

        public interface IBaseViewModel : INotifyPropertyChanged
        {
            object ModelData { get; set; }
            IDirectData DirectDataToView { get; set; }
            void LoadData();
            void Subscribe(IInforming informingView);
            void Subscribe(ILaunchChild launchingView);
        }

It also implements few common methods and properties such as InfoText, SubmitCommand, PopUpView, and handling the NotifyEvent for View in View request.

Commands

The CommandBase is a class that simplifies creating a command handler. It allows me to pass two delegates one for the Can Execute and on for the Execute itself.

Summary

With the small VvmScaffolding project it is possible to get some basic View-ViewModel benefits.

It is simple enough for me to modify it and build upon it as the need arise. My next step is to try and get commands to work on other controls and other events besides click. The MVVM design pattern is set of recommendations not a set of rules...

Up Next
    Ebook Download
    View all
    Learn
    View all