How to get Label view in ViewModel to set accessibility focus in xamarin forms - xamarin

I have Label in view, I need that Label's view in my ViewModel. I am using Dependency Service to set focus on Controls for Accessibility service, DS requires view as a param.
This is my Label
<Label
AutomationProperties.IsInAccessibleTree="{Binding ShowNoResults}"
IsVisible="{Binding ShowNoResults}"
Text="{Binding ResultsHeader}"/>
I tried Command but Label doesn't support command. Below code also not working
var view = GetView() as HomeworkView;
I am getting view always null. How can I fix this?

I am not quite sure what are you trying to achieve, but you can't access the View elements from you view model.
If you want to do something with the control, you can use the messaging center to do it, here is an example
in your ViewModel
MessagingCenter.Send(this, "your message here");
then in your page, you need to subscribe to this message from that view model and do the desired action
MessagingCenter.Instance.Unsubscribe<ViewModelClassNamedel>(this, "your message here");
MessagingCenter.Instance.Subscribe<ViewModelClassName>(this, "your message here", (data) =>
{
this.youControlName.Focus();
});

More detail added to Mohammad's answer.
Message Center doc.
In your ViewModel (with class name "YourViewModel"):
// Here we use control name directly.
// OR could make an "enum" with a value for each control.
string controlName = ...;
MessagingCenter.Send<YourViewModel>(this, "focus", controlName);
then in your page, subscribe to this message and do the desired action
.xaml.cs:
protected override void OnAppearing() {
{
base.OnAppearing();
// Unsubscribe before Subscribe ensures you don't Subscribe twice, if the page is shown again.
MessagingCenter.Instance.Unsubscribe<YourViewModel>(this, "focus");
MessagingCenter.Instance.Subscribe<YourViewModel>(this, "focus", (controlName) =>
{
View v = null;
switch (controlName) {
case "name1":
v = this.name1;
break;
case "name2":
v = this.name2;
break;
}
if (v != null) {
//v.Focus();
// Tell VM to use v as view.
((YourViewModel)BindingContext).SetFocus(v);
}
});
}
protected override void OnDisappearing() {
MessagingCenter.Instance.Unsubscribe<YourViewModel>(this, "focus");
base.OnDisappearing();
}
If need to pass View v back to VM, because that has the logic to use it:
public class YourViewModel
{
public void SetFocus(View view)
{
... your code that needs label's view ...
}
}
Not tested. Might need some slight changes. Might need
...(this, "focus", (sender, controlName) =>
instead of
...(this, "focus", (controlName) =>
UPDATE
Simple approach, if there is only ONE View that is needed in VM.
public class YourViewModel
{
public View ViewToFocus { get; set; }
// The method that needs ViewToFocus.
void SomeMethod()
{
...
if (ViewToFocus != null)
... do something with it ...
}
}
public class YourView
{
public YourView()
{
InitializeComponent();
...
// After BindingContext is set.
((YourViewModel)BindingContext).ViewToFocus = this.yourLabelThatShouldBeFocused;
}
}
ALTERNATIVE: It might be cleaner/more robust to set ViewToFocus in page's OnAppearing, and clear it in OnDisappearing. This ensures it is never used while the page is not visible (or in some delayed action after the page has gone away). I would probably do it this way.
protected override void OnAppearing()
{
base.OnAppearing();
((YourViewModel)BindingContext).ViewToFocus = this.yourLabelThatShouldBeFocused;
}
protected override void OnDisappearing()
{
((YourViewModel)BindingContext).ViewToFocus = null;
base.OnDisappearing();
}

Related

How to Refresh the Bindings on HomePageViewModel when some data has been changed in Navigated Page2

I have some property which I am using from Page2 in HomePageViewModel, When I navigate to Page2 I have changed that property and on coming back by doing NavigiationPop.
HomePage is not re-loaded/ refreshed at all as I have set the BindingContext in the constructor of HomePage, which is loaded only once.
The solution which is working for me is
Setting the BindingContext to the ViewModel on "onAppearing()", usually its not consider the best practice. But I also need to refresh the values which I have changed on Page2.
Looking forward for your valuable inputs and suggestions.
Also Pros/Cons of Setting the BindingContext in OnAppearing.
Thanks,
Hemant
It depends on you, they all can achieve the effect.
However if you want to reduce the CPU consumption of the program, then don't use the onAppearing method too much to refresh the page data.
Another method is that you can change the data of the model through Delegate/Event/MessageCenter.
You can refer to this discussion to know how to use Delegate/Event/MessageCenter.
Here I will give the sample by using MessageCenter to achieve that.
For example, the TestViewModel as follows:
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string pagetitle;
public string PageTitle
{
get
{
return pagetitle;
}
set
{
if (pagetitle != value)
{
pagetitle = value;
NotifyPropertyChanged();
}
}
}
}
Then bind it in FirstPage as follows, and Subscribe MessageCenter inside it:
TestViewModel testViewModel = new TestViewModel();
BindingContext = testViewModel;
MessagingCenter.Subscribe<object>(this, "Hi", (sender) =>
{
// Do something whenever the "Hi" message is received
testViewModel.PageTitle = "Modified Title";
});
Then in the SecondPage, when changing the data by sending message. When back to FirstPage, the View Will be updated.
private void Button_Clicked(object sender, EventArgs e)
{
MessagingCenter.Send<object>(this, "Hi");
}

Create Behavior from Map Property in Control - Xamarin Forms MVVM Prism ArcGIS

I'm trying to show a callout when navigating from View A (form view) to View B (map view). I pass the record detail for the callout from View A to View B and it's bound to viewmodel of View B.
I would like to access the LoadStatusChanged event on the Map property of the Esri MapView control while adhering to MVVM architecture. Here is what my control looks like:
<esri:MapView x:Name="mapViewMain"
Map="{Binding MainMap}"
Grid.Row="0"
InteractionOptions="{Binding MapViewOptions}"
GraphicsOverlays="{Binding GraphicsOverlays}">
<esri:MapView.Behaviors>
<bh:ShowCalloutOnTapBehavior CalloutClickCommand="{Binding GoToDetailCommand}" />
<bh:ShowCalloutOnDataReceivedBehavior MeterMasterRequest="{Binding RequestParameters}" Map="{Binding MainMap}" />
</esri:MapView.Behaviors>
</esri:MapView>
I think I need to create a behavior that will take in the Map, wait for it to finish load, then show callout (results of MeterMasterRequest) on MapView (the ShowCallout method is on MapView control).
public class ShowCalloutOnDataReceivedBehavior : BehaviorBase<MapView>
{
public static readonly BindableProperty MeterMasterRequestProperty =
BindableProperty.Create(nameof(MeterMasterRequest), typeof(MeterMasterRequest), typeof(ShowCalloutOnDataReceivedBehavior));
public static readonly BindableProperty MapProperty =
BindableProperty.Create(nameof(Map), typeof(Map), typeof(ShowCalloutOnDataReceivedBehavior));
public MeterMasterRequest MeterMasterRequest
{
get { return (MeterMasterRequest)GetValue(MeterMasterRequestProperty); }
set { SetValue(MeterMasterRequestProperty, value); }
}
public Map Map
{
get { return (Map)GetValue(MapProperty); }
set { SetValue(MapProperty, value); }
}
How can I bind to the Map event from here? I don't know how I can get BehaviorBase to be of type Map. I seem to only be able to set behaviors at MapView level.
You can access the Map from the MapView.Map property and the MapView is passed in to the OnAttachedTo and OnDetachingFrom methods of the Behavior class. I see you have a BehaviorBase, which hopefully has the OnAttachedTo and OnDetachingFrom overrides still marked as protected virtual so you can override them in your ShowCalloutOnDataReceivedBehavior class
Override the OnAttachedTo method and then subscribe to the LoadStatusChanged event, override OnDetachingFrom so you can unsubscribe like so:
protected override void OnAttachedTo(MapView bindable)
{
base.OnAttachedTo(bindable);
bindable.Map.LoadStatusChanged += Map_LoadStatusChanged;
}
protected override void OnDetachingFrom(MapView bindable)
{
base.OnDetachingFrom(bindable);
bindable.Map.LoadStatusChanged -= Map_LoadStatusChanged;
}
private void Map_LoadStatusChanged(object sender, Esri.ArcGISRuntime.LoadStatusEventArgs e)
{
// Do stuff when LoadStatusChanged event is fired on the Map
}

How to implement a custom presenter in a Windows UWP (Xamarin, MvvmCross)

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

Set different binding context for custom user control

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;
}
}

Receive Notifications from Child ViewModels

I might be missing something simple, so bear with me.
I have a ViewModel that contains the following:
public ObservableCollection<Person> PersonCollection
{
get { return personCollection; }
set
{
if (personCollection != value)
{
personCollection = value;
RaisePropertyChanged("PersonCollection");
}
}
}
Then in another ViewModel I have:
public ObservableCollection<Person> PersonCollection
{
get
{
PersonViewModel vm = (App.Current.Resources["Locator"] as ViewModelLocator).PersonViewModel;
return vm.PersonCollection;
}
}
public PersonViewModel PersonViewModel
{
get
{
return ((App.Current.Resources["Locator"] as ViewModelLocator).PersonViewModel)
}
}
In my XAML if I bind to PersonCollection then updates don't happen on my view, but if I bind to PersonViewModel.PersonCollection it does. so is this the "proper" way to do it or is there a way for the view to detect the notifications using the first approach?
Change your binding to {Binding PersonViewModel.PersonCollection}
Your wrapped PersonCollection property has no change notifications, so the view doesn't know that the property has changed (it certainly has no way of knowing it originally came from PersonViewModel in order to get change notifications from it)

Resources