In Xamarin Forms v4.0.0.394984 pre10 using Visual Studio 2017 I was able to add MenuItems to the Shell programatically in the constructor of the Shell using the MenuItemsCollection MenuItems.
Here is the code that works in v4.0.0.394984 pre10 (simplified it for this question to add a single hard-coded menuitem and function LoadView is not shown here)
public partial class Shell : Xamarin.Forms.Shell
{
public ICommand cmdLoadView { get; } = new Command(LoadView);
public Shell()
{
InitializeComponent();
BindingContext = this;
MenuItem mi = new MenuItem();
mi.Command = cmdLoadView;
mi.CommandParameter = "myCommand";
mi.Text = "sampleName";
MenuItems.Add(mi);
}
...
}
In all subsequent versions, this code does not work. Intellisense indicates that MenuItems is still part of the Shell, but I get a compilation error saying:
error CS0103: The name 'MenuItems' does not exist in the current context.
When I reference as this.MenuItems I get the compilation error:
error CS1061: 'Shell' does not contain a definition for 'MenuItems' and no accessible extension method 'MenuItems' accepting a first argument of type 'Shell' could be found (are you missing a using directive or an assembly reference?)
Is this possible in the current version of Xamarin.Forms? I've tried with each of the releases and pre-releases since v4 pre10 and none have worked.
Thanks is advance for any help!
You need to add the MenuItem objects to the Current.Items property in your AppShell implementation. Example:
// factory method to create MenuItem objects
private static MenuItem CreateMenuItem(string title, ICommand cmd)
{
var menuItem = new MenuItem();
menuItem.Text = title;
menuItem.Command = cmd;
return menuItem;
}
public AppShell()
{
...
// you can place this code in any method in the AppShell class
Current.Items.Add(CreateMenuItem("AppInfo", new Command(async () =>
{
ShellNavigationState state = Shell.Current.CurrentState;
await Shell.Current.Navigation.PushAsync(new AppInfoPage());
Shell.Current.FlyoutIsPresented = false;
})));
...
}
Related
I want to open a pop up window using xamarin comunity toolkit extensions but when i try to call
Navigation it says that it does not exist even if i have included xct.extensions. There is NavigationExtensions but I don't know what I'm supposed to pass as first parameter and it says that my FilterPopup is of incorect type
using Xamarin.CommunityToolkit.Extensions;
namespace Appointments.ViewModels
{
public class WallViewModel
{
public ObservableCollection<Stylist> Stylists { get; set; }
public AsyncCommand OpenModalFiltersComand;
public WallViewModel()
{
OpenModalFiltersComand = new AsyncCommand(OpenModalFilters);
}
async Task OpenModalFilters()
{
NavigationExtensions.ShowPopup(AppShell, FilterPopup);
Navigation // error
}
}
}
My FilterPopup
namespace Appointments.Popups
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class FilterPopup : Popup
{
public FilterPopup()
{
InitializeComponent();
}
}
}
EDIT---------
I'm using shell navigation
ShowPopup takes an INavigation and an instance of a PopupBase. Navigation is a property of Page types, so your VM does not have a reference to it.
var nav = App.Current.MainPage.Navigation;
var filter = new FilterPopup();
NavigationExtensions.ShowPopup(nav, filter);
I am trying to find a way to be able to set from the View to what ViewModel I have to navigate. This is to be able to change the navigation flow without changing the core project.
I thought the easier way would be creating an interface, setting the target ViewModel there and injecting the interface into the ViewModel to then perform the navigation.
public interface IModelMapping
{
MvxViewModel ViewModelToNavigate();
}
public class MyViewModel : MvxViewModel
{
readonly IMvxNavigationService navigationService;
readonly IModelMapping modelMapping;
public MyViewModel(IMvxNavigationService navigationService, IModelMapping modelMapping)
{
this.navigationService = navigationService;
this.modelMapping = modelMapping;
}
public IMvxAsyncCommand GoContent
{
get
{
IMvxViewModel vm = modelMapping.ViewModelToNavigate();
IMvxAsyncCommand navigateCommand = new MvxAsyncCommand(() => navigationService.Navigate<vm>());
return navigteCommand;
}
}
}
The problem with this code is I am getting an error setting the navigationService.Navigate(). The error is 'vm is a variable but it is used like a type'
What about using the URI navigation together with the facade? See also https://www.mvvmcross.com/documentation/fundamentals/navigation#uri-navigation
Say you are building a task app and depending on the type of task you want to show a different view. This is where NavigationFacades come in handy (there is only so much regular expressions can do for you).
mvx://task/?id=00 <– this task is done, show read-only view (ViewModelA)
mvx://task/?id=01 <– this task isn’t, go straight to edit view (ViewModelB)
[assembly: MvxRouting(typeof(SimpleNavigationFacade), #"mvx://task/\?id=(?<id>[A-Z0-9]{32})$")]
namespace *.NavigationFacades
{
public class SimpleNavigationFacade
: IMvxNavigationFacade
{
public Task<MvxViewModelRequest> BuildViewModelRequest(string url,
IDictionary<string, string> currentParameters, MvxRequestedBy requestedBy)
{
// you can load data from a database etc.
// try not to do a lot of work here, as the user is waiting for the UI to do something ;)
var viewModelType = currentParameters["id"] == Guid.Empty.ToString("N") ? typeof(ViewModelA) : typeof(ViewModelB);
return Task.FromResult(new MvxViewModelRequest(viewModelType, new MvxBundle(), null, requestedBy));
}
}
}
I have a CalendarView that looks like this:
<CalendarView
android:layout_width="match_parent"
android:layout_height="300dp"
android:id="#+id/createReservationCalendarView" />
Here is how I handle the DateChange event without MvvmCross:
protected override void OnCreate(Bundle savedInstanceState)
{
... Code ...
calendar.DateChange += (s, args) =>
{
var year = args.Year;
var month = args.Month + 1;
var dayOfMont = args.DayOfMonth;
var date = new DateTime(year, month, dayOfMont);
var myReservations = new Intent(this, typeof(CreateReservationTimeslotScreen));
myReservations.PutExtra("selectedDate", date.Ticks);
StartActivity(myReservations);
};
}
Now that I have switched to MvvmCross, I would like to have my ViewModel start the new activity instead.
Im not sure how to do this, since the ViewModel should be OS and UI agnostic.
The "args" argument is of type CalendarView.DateChangeEventArgs, which is Android specific, so I cant use that in the ViewModel. It derives from System.EventArgs, so maybe I could use that instead. I am thinking that there must be a simpler way.
A thought that I had was if it is possible to update a property on the ViewModel from the activity, and then execute the switch to the new Activity from there? I'm not sure how this could be accomplished since activites dont have references to their ViewModels.
Any suggestions?
Thanks.
MvvmCross does give you access to your ViewModel from your View. The relationship between your View (e.g. Activity/fragment in Android) and your ViewModel, and their ability to share data (models) in both directions is a core characteristic a Mvvm framework.
In order to setup an Activity to be used with MvvmCross you need to make sure to inherit from MvxActivity or MvxAppCompatActivity (If using Android Support Library). Following which you need to link your Activity to its corresponding ViewModel using one of the possible conventions (See link, for basic sample of each registration offered by the MvxViewModelViewTypeFinder). A simple example would be to use the concrete type based registration using the type parameter overload.
public class FirstActivity : MvxAppCompatActivity<FirstViewModel>
Now that you have access to your ViewModel from your View you can create a command that can be used to execute the navigation:
CalendarViewModel (ViewModel linked to the current Activity in question)
Create a command that requires a DateTime parameter, which in turn will pass the value when navigation (see MvvmCross Navigation docs for alternative navigation and parameter passing conventions).
public class CalendarViewModel : MvxViewModel
{
IMvxCommand _goToMyReservationCommand;
public IMvxCommand GoToMyReservationCommand =>
_goToMyReservationCommand ??
(_goToMyReservationCommand = new MvxCommand<DateTime>(NavigateToMyReservation));
void NavigateToMyReservation(DateTime reservationDate)
{
ShowViewModel<MyReservationViewModel>(
new GoToMyReservationParameter
{
ReservationTicks = reservationDate.Ticks
});
}
}
Navigation Parameter Class
Holds the values and type information used for navigation.
public class GoToMyReservationParameter
{
public long ReservationTicks { get; set; }
}
MyReservationViewModel
The ViewModel that will receive the value passed.
public class MyReservationViewModel : MvxViewModel
{
public void Init(GoToMyReservationParameter parameters)
{
var reservationTicks = parameters.ReservationTicks;
// Do what you need with the parameters
}
}
View
Execute the command on the ViewModel and pass through the DateTime object.
public class CalendarActivity : MvxAppCompatActivity<CalendarViewModel>
{
protected override void OnCreate(Bundle bundle)
{
... Code...
calendar.DateChange += (s, args) =>
{
var year = args.Year;
var month = args.Month + 1;
var dayOfMont = args.DayOfMonth;
var date = new DateTime(year, month, dayOfMont);
ViewModel.GoToMyReservationCommand.Execute(date);
};
}
}
I have the following code in my Android app, it basically uses one page (using a NavigationDrawer) and swaps fragments in/out of the central view. This allows the navigation to occur on one page instead of many pages:
Setup.cs:
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var customPresenter = new MvxFragmentsPresenter();
Mvx.RegisterSingleton<IMvxFragmentsPresenter>(customPresenter);
return customPresenter;
}
ShellPage.cs
public class ShellPage : MvxCachingFragmentCompatActivity<ShellPageViewModel>, IMvxFragmentHost
{
.
.
.
public bool Show(MvxViewModelRequest request, Bundle bundle)
{
if (request.ViewModelType == typeof(MenuContentViewModel))
{
ShowFragment(request.ViewModelType.Name, Resource.Id.navigation_frame, bundle);
return true;
}
else
{
ShowFragment(request.ViewModelType.Name, Resource.Id.content_frame, bundle, true);
return true;
}
}
public bool Close(IMvxViewModel viewModel)
{
CloseFragment(viewModel.GetType().Name, Resource.Id.content_frame);
return true;
}
.
.
.
}
How can I achieve the same behavior in a Windows UWP app? Or rather, is there ANY example that exists for a Windows MvvmCross app which implements a CustomPresenter? That may at least give me a start as to how to implement it.
Thanks!
UPDATE:
I'm finally starting to figure out how to go about this with a customer presenter:
public class CustomPresenter : IMvxWindowsViewPresenter
{
IMvxWindowsFrame _rootFrame;
public CustomPresenter(IMvxWindowsFrame rootFrame)
{
_rootFrame = rootFrame;
}
public void AddPresentationHintHandler<THint>(Func<THint, bool> action) where THint : MvxPresentationHint
{
throw new NotImplementedException();
}
public void ChangePresentation(MvxPresentationHint hint)
{
throw new NotImplementedException();
}
public void Show(MvxViewModelRequest request)
{
if (request.ViewModelType == typeof(ShellPageViewModel))
{
//_rootFrame?.Navigate(typeof(ShellPage), null); // throws an exception
((Frame)_rootFrame.UnderlyingControl).Content = new ShellPage();
}
}
}
When I try to do a navigation to the ShellPage, it fails. So when I set the Content to the ShellPage it works, but the ShellPage's ViewModel is not initialized automatically when I do it that way. I'm guessing ViewModels are initialized in MvvmCross using OnNavigatedTo ???
I ran into the same issue, and built a custom presenter for UWP. It loans a couple of ideas from an Android sample I found somewhere, which uses fragments. The idea is as follows.
I have a container view which can contain multiple sub-views with their own ViewModels. So I want to be able to present multiple views within the container.
Note: I'm using MvvmCross 4.0.0-beta3
Presenter
using System;
using Cirrious.CrossCore;
using Cirrious.CrossCore.Exceptions;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.Views;
using Cirrious.MvvmCross.WindowsUWP.Views;
using xxxxx.WinUniversal.Extensions;
namespace xxxxx.WinUniversal.Presenters
{
public class MvxWindowsMultiRegionViewPresenter
: MvxWindowsViewPresenter
{
private readonly IMvxWindowsFrame _rootFrame;
public MvxWindowsMultiRegionViewPresenter(IMvxWindowsFrame rootFrame)
: base(rootFrame)
{
_rootFrame = rootFrame;
}
public override async void Show(MvxViewModelRequest request)
{
var host = _rootFrame.Content as IMvxMultiRegionHost;
var view = CreateView(request);
if (host != null && view.HasRegionAttribute())
{
host.Show(view as MvxWindowsPage);
}
else
{
base.Show(request);
}
}
private static IMvxWindowsView CreateView(MvxViewModelRequest request)
{
var viewFinder = Mvx.Resolve<IMvxViewsContainer>();
var viewType = viewFinder.GetViewType(request.ViewModelType);
if (viewType == null)
throw new MvxException("View Type not found for " + request.ViewModelType);
// Create instance of view
var viewObject = Activator.CreateInstance(viewType);
if (viewObject == null)
throw new MvxException("View not loaded for " + viewType);
var view = viewObject as IMvxWindowsView;
if (view == null)
throw new MvxException("Loaded View is not a IMvxWindowsView " + viewType);
view.ViewModel = LoadViewModel(request);
return view;
}
private static IMvxViewModel LoadViewModel(MvxViewModelRequest request)
{
// Load the viewModel
var viewModelLoader = Mvx.Resolve<IMvxViewModelLoader>();
return viewModelLoader.LoadViewModel(request, null);
}
}
}
IMvxMultiRegionHost
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.WindowsUWP.Views;
namespace xxxxx.WinUniversal.Presenters
{
public interface IMvxMultiRegionHost
{
void Show(MvxWindowsPage view);
void CloseViewModel(IMvxViewModel viewModel);
void CloseAll();
}
}
RegionAttribute
using System;
namespace xxxxx.WinUniversal.Presenters
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class RegionAttribute
: Attribute
{
public RegionAttribute(string regionName)
{
Name = regionName;
}
public string Name { get; private set; }
}
}
These are the three foundational classes you need. Next you'll need to implement the IMvxMultiRegionHost in a MvxWindowsPage derived class.
This is the one I'm using:
HomeView.xaml.cs
using System;
using System.Diagnostics;
using System.Linq;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.WindowsUWP.Views;
using xxxxx.Shared.Controls;
using xxxxx.WinUniversal.Extensions;
using xxxxx.WinUniversal.Presenters;
using xxxxx.Core.ViewModels;
namespace xxxxx.WinUniversal.Views
{
public partial class HomeView
: MvxWindowsPage
, IMvxMultiRegionHost
{
public HomeView()
{
InitializeComponent();
}
// ...
public void Show(MvxWindowsPage view)
{
if (!view.HasRegionAttribute())
throw new InvalidOperationException(
"View was expected to have a RegionAttribute, but none was specified.");
var regionName = view.GetRegionName();
RootSplitView.Content = view;
}
public void CloseViewModel(IMvxViewModel viewModel)
{
throw new NotImplementedException();
}
public void CloseAll()
{
throw new NotImplementedException();
}
}
}
The last piece to make this work is the way the actual xaml in the view is set-up. You'll notice that I'm using a SplitView control, and that I'm replacing the Content property with the new View that's coming in in the ShowView method on the HomeView class.
HomeView.xaml
<SplitView x:Name="RootSplitView"
DisplayMode="CompactInline"
IsPaneOpen="false"
CompactPaneLength="48"
OpenPaneLength="200">
<SplitView.Pane>
// Some ListView with menu items.
</SplitView.Pane>
<SplitView.Content>
// Initial content..
</SplitView.Content>
</SplitView>
EDIT:
Extension Methods
I forgot to post the two extension methods to determine if the view declares a [Region] attribute.
public static class RegionAttributeExtentionMethods
{
public static bool HasRegionAttribute(this IMvxWindowsView view)
{
var attributes = view
.GetType()
.GetCustomAttributes(typeof(RegionAttribute), true);
return attributes.Any();
}
public static string GetRegionName(this IMvxWindowsView view)
{
var attributes = view
.GetType()
.GetCustomAttributes(typeof(RegionAttribute), true);
if (!attributes.Any())
throw new InvalidOperationException("The IMvxView has no region attribute.");
return ((RegionAttribute)attributes.First()).Name;
}
}
Hope this helps.
As the link to the blog of #Stephanvs is no longer active I was able to pull the content off the Web Archive, i'll post it here for who ever is looking for it:
Implementing a Multi Region Presenter for Windows 10 UWP and MvvmCross
18 October 2015 on MvvmCross, Xamarin, UWP, Windows 10, Presenter > Universal Windows Platform
I'm upgrading a Windows Store app to the new Windows 10 Universal
Windows Platform. MvvmCross has added support for UWP in v4.0-beta2.
A new control in the UWP is the SplitView control. Basically it
functions as a container view which consist of two sub views, shown
side-by-side. Mostly it's used to implement the (in)famous hamburger
menu.
By default MvvmCross doesn't know how to deal with the SplitView, and
just replaces the entire screen contents with a new View when
navigating between ViewModels. If however we want to lay-out our views
differently and show multiple views within one window, we need a
different solution. Luckily we can plug-in a custom presenter, which
will take care of handling the lay-out per platform.
Registering the MultiRegionPresenter
In the Setup.cs file in your UWP project, you can override the
CreateViewPresenter method with the following implementation.
protected override IMvxWindowsViewPresenter CreateViewPresenter(IMvxWindowsFrame rootFrame)
{
return new MvxWindowsMultiRegionViewPresenter(rootFrame);
}
Using Regions
We can define a region by declaring a
element. At this point it has to be a Frame type because then we can
also show a nice transition animation when switching views.
<mvx:MvxWindowsPage ...>
<Grid>
<!-- ... -->
<SplitView>
<SplitView.Pane>
<!-- Menu Content as ListView or something similar -->
</SplitView.Pane>
<SplitView.Content>
<Frame x:Name="MainContent" />
</SplitView.Content>
</SplitView>
</Grid>
</mvx:MvxWindowsPage>
Now we want to be able when a ShowViewModel(...) occurs to swap out
the current view presented in the MainContent frame.
Showing Views in a Region
In the code-behind for a View we can now declare a MvxRegionAttribute,
defining in which region we want this View to be rendered. This name
has to match a Frame element in the view.
[MvxRegion("MainContent")]
public partial class PersonView
{
// ...
}
It's also possible to declare multiple regions within the same view.
This would allow you to split up your UI in more re-usable pieces.
Animating the Transition between Content Views
If you want a nice animation when transitioning between views in the
Frame, you can add the following snippet to the Frame declaration.
<Frame x:Name="MainContent">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition>
<NavigationThemeTransition.DefaultNavigationTransitionInfo>
<EntranceNavigationTransitionInfo />
</NavigationThemeTransition.DefaultNavigationTransitionInfo>
</NavigationThemeTransition>
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>
The contents will now be nicely animated when navigating.
Hope this helps, Stephanvs
I am trying to create my first Xamarin.Forms custom user control named LocationSelector. It has an Entry and when the user enters something, a list with selectable locations is shown (similary to the selection in Google Maps).
The selected location is the important 'return value' of the control.
My plan is to catch the ItemSelected event of the list and set the SelectedLocation property. LocationSelector is designed as MVVM and since everything is working so far here just the Code-Behind (which I think is enough to describe the problem):
public partial class LocationSelector : StackLayout
{
public static readonly BindableProperty SelectedLocationProperty =
BindableProperty.Create<LocationSelector, LocationModel>(s => s.SelectedLocation, new LocationModel(), BindingMode.TwoWay);
public LocationSelector()
{
InitializeComponent();
var model = new LocationSelectorModel();
BindingContext = model;
_listView.ItemSelected += (sender, args) =>
{
SelectedLocation = model.SelectedLocation;
};
}
public LocationModel SelectedLocation
{
get { return (LocationModel)GetValue(SelectedLocationProperty); }
set { SetValue(SelectedLocationProperty, value); }
}
}
Now I want to use this control on a search view where the BindingContext is set to the SearchViewModel:
<ContentPage x:Class="Application.App.Views.SearchView" ...>
<c:LocationSelector SelectedLocation="{Binding Location}"/>
</ContentPage>
public class SearchViewModel : ViewModel
{
private LocationModel _location;
public LocationModel Location
{
get { return _location; }
set { SetProperty(ref _location, value); }
}
}
Unfortunately this is not working. The output throws a binding warning:
Binding: 'Location' property not found on 'Application.App.CustomControls.LocationSelectorModel', target property: 'Application.App.CustomControls.LocationSelector.SelectedLocation'
Why points the binding to a property in the ViewModel that is used 'within' my custom control and not to the property in the BindingContext of the view?
The problem is setting the BindingContext to the view model of the user control:
public LocationSelector()
{
var model = new LocationSelectorModel();
BindingContext = model; // this causes the problem
// ...
}
In this post I found the solution. Setting the BindingContext to each child elements rather than the whole user control is doing the job:
public LocationSelector()
{
var model = new LocationSelectorModel();
foreach (var child in Children)
{
child.BindingContext = model;
}
}