NullReference when Show a MvxTabBarViewController mvvmcross ios - ios

I have a storyboard made on xCode, and know I'm working on visual studio with Xamarin and mvvmcross, to do the IOS and the Android app with the same Core.
My StoryBoard have some views (including a SideBar on some of the initial views) that will open 4 views that will call ShowViewModel for a MvxTabBarViewController, and each one of that view will open a different tab.
All the Navigation to that 4 view, its done, now I have problems Showing the Tab Bar...
When I try to do a simple test of call ShowViewModel to MvxTabBarViewController and open a specific tab, on the ViewDidLoad of that tab, I have the error
System.NullReferenceException: Object reference not set to an instance
of an object
The structure of my Tab bar was all created on the storyboard.
Here is the code of my TabBarController:
[MvxFromStoryboard("Main")]
[MvxSidebarPresentation(MvxPanelEnum.Center, MvxPanelHintType.PushPanel, true)]
public partial class TabDetailView : MvxTabBarViewController<TabDetailViewModel>
{
private bool _constructed;
public TabDetailView(IntPtr handle) : base(handle)
{
_constructed = true;
// need this additional call to ViewDidLoad because UIkit creates the view before the C# hierarchy has been constructed
ViewDidLoad();
}
public TabDetailView()
{
_constructed = true;
// need this additional call to ViewDidLoad because UIkit creates the view before the C# hierarchy has been constructed
ViewDidLoad();
}
public override void ViewDidLoad()
{
if (!_constructed)
return;
base.ViewDidLoad();
var vm = (TabDetailViewModel)this.ViewModel;
if (vm == null)
return;
}
}
And Here is the code of my ViewController corresponding to the Tab that I want to open:
[MvxFromStoryboard("Main")]
[MvxSidebarPresentation(MvxPanelEnum.Center, MvxPanelHintType.PushPanel, true)]
public partial class TabDiaryView : MvxViewController<TabDiaryViewModel>
{
public TabDiaryView (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad(); //HERE IS THE ERROR (NULLREFERENCE)
}
}
public class Setup : MvxIosSetup
{
public Setup(MvxApplicationDelegate applicationDelegate, UIWindow window)
: base(applicationDelegate, window)
{
}
public Setup(MvxApplicationDelegate applicationDelegate, IMvxIosViewPresenter presenter)
: base(applicationDelegate, presenter)
{
}
protected override IMvxApplication CreateApp()
{
return new Core.App();
}
protected override IMvxTrace CreateDebugTrace()
{
return new DebugTrace();
}
protected override IMvxIosViewPresenter CreatePresenter()
{
return new MvxSidebarPresenter((MvxApplicationDelegate)ApplicationDelegate, Window);
}
}
Please help me!!

Related

Xamarin IOS custom control property doesn't displays

I've created my own custom control for my Xamarin.IOS application and I created a custom property for it.
And I want to set value for it property in Visual Studio Properties toolbox, but no VS no Xcode doesn't display any custom properties.
I have to set custom Text kern spacing for custom buttons and I don't want to use any tricky or ugly ways to sort it out.
Here is my custom control code:
[Register("CustomButton"), DesignTimeVisible(true)]
public class CustomButton : UIButton
{
private float _textLetterSpacing;
protected internal CustomButton(IntPtr handle) : base(handle)
{
}
[Export("TextLetterSpacing"), Browsable(true)]
public float TextLetterSpacing
{
get => _textLetterSpacing;
set
{
_textLetterSpacing = value;
SetNeedsDisplay();
}
}
public CustomButton()
{
Initialize();
}
public override void AwakeFromNib()
{
// Called when loaded from xib or storyboard.
Initialize();
}
void Initialize()
{
TextLetterSpacing = -0.49f;
var title = this.Title(UIControlState.Normal);
var attributedTitle = new NSAttributedString(title, new UIStringAttributes() { KerningAdjustment = TextLetterSpacing, ForegroundColor = this.CurrentTitleColor });
this.SetAttributedTitle(attributedTitle, UIControlState.Normal);
SetNeedsDisplay();
}
}
I figured out this issue.
There should public constructor instead of protected internal:
public CustomButton(IntPtr handle) : base(handle)
{
}

How to bind Toolbar MenuItem.IsEnabled with MvvmCross and Xamarin.Android

Using MvvmCross and Xamarin.Android is it possible to bind the IsEnabled property of a Toolbar MenuItem to a boolean value in a viewmodel? If so, how is it done?
I believe it's impossible at this time to bind Android IMenuItem.IsEnabled to a boolean value on a viewmodel due to the fact that IsEnabled is readonly and changed the enabled state of a menu item requires a call to SetIsEnabled(bool).
I worked around this limitation by adding an event handler for MvxViewModel.PropertyChanged in the Activity/Fragment. All my viewmodels inherit from MvxViewModel so I'll share a way this can be accomplished. Most of my app is implemented with Fragments, so my example reflects that, (in my actual code I've put most of this in a base Fragment class, but I wanted to keep it simple):
public class MyViewModel : MvxViewModel
{
public bool MenuItemIsEnabled {
get{return _menuItemIsEnabled;}
set{SetProperty(ref _menuItemIsEnabled, value);
}
}
[MvxFragment(typeof(MainViewModel), Resource.Id.content_frame, true)]
[Register(nameof(MyFragment))]
public class MyFragment : MvxFragment
{
private Toolbar Toolbar;
//menuitem whose enabled state should change with viewmodel property
private IMenuItem MyMenuItem;
public new MyViewModel ViewModel
{
get { return (MyViewModel)base.ViewModel; }
set { base.ViewModel = value; }
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = base.OnCreateView(inflater, container, savedInstanceState);
//we'll need the Toolbar later when we set up the IsEnabled "binding"
Toolbar = view.FindViewById<Toolbar>(Resource.Id.toolbar);
return view;
}
public override void OnViewModelSet()
{
base.OnViewModelSet();
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
public override void OnCreateOptionsMenu(IMenu menu, MenuInflater inflater)
{
base.OnCreateOptionsMenu(menu, inflater);
//create the menu based on a menu resource
inflater.Inflate(Resource.Menu.my_menu, menu);
//save a reference to the menu item so we can update it when the viewmodel changes
MyMenuItem = menu.FindItem(Resource.Id.my_menu_item);
//set the initial enabled state based on the viewmodel
MyMenuItem.SetEnabled(ViewModel.MenuItemIsEnabled);
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
//when ViewModel.MenuItemIsEnabled is updated, update the menuitem enabled state as well
if (e.PropertyName == nameof(ViewModel.MenuItemIsEnabled ))
BinContentsMenuItem?.SetEnabled(ViewModel.MenuItemIsEnabled);
}
}
This should be considered pseudocode, since it's trying to demonstrate the general concepts I used to work around my original problem.

iOS: Camera does not recognize QR Code on first load of view controller

I have a small app, that does read QR-Codes for a login and alternatively offers the possibility to hand-type the code and login.
The app starts and heads directly to the login (View). When I try to scan a qr code that does not work - the delegate is never called/the event never raised.
I adapted the approach from Larry OBrien http://www.knowing.net/index.php/2013/10/09/natively-recognize-barcodesqr-codes-in-ios-7-with-xamarin-ios/
And created my own ScannerView class for that use:
public sealed partial class ScannerView : UIView
{
private readonly AVCaptureVideoPreviewLayer _layer;
public AVCaptureSession Session { get; }
private readonly AVCaptureMetadataOutput _metadataOutput;
public event EventHandler<AVMetadataMachineReadableCodeObject> MetadataFound = delegate { };
public ScannerView (IntPtr handle) : base (handle)
{
Session = new AVCaptureSession();
var camera = AVCaptureDevice.DefaultDeviceWithMediaType(AVMediaType.Video);
var input = AVCaptureDeviceInput.FromDevice(camera);
Session.AddInput(input);
//Add the metadata output channel
_metadataOutput = new AVCaptureMetadataOutput {RectOfInterest = Bounds};
var metadataDelegate = new MetadataOutputDelegate();
var dispatchQueue = new DispatchQueue("scannerQueue");
_metadataOutput.SetDelegate(metadataDelegate, dispatchQueue);
Session.AddOutput(_metadataOutput);
_layer = new AVCaptureVideoPreviewLayer(Session)
{
MasksToBounds = true,
VideoGravity = AVLayerVideoGravity.ResizeAspectFill,
Frame = Bounds
};
Layer.AddSublayer(_layer);
// Hand event over to subscriber
metadataDelegate.MetadataFound += (s, e) => MetadataFound(s, e);
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
_layer.Frame = Bounds;
_metadataOutput.RectOfInterest = Bounds;
}
public void SetMetadataType(AVMetadataObjectType type)
{
//Confusing! *After* adding to session, tell output what to recognize...
_metadataOutput.MetadataObjectTypes = type;
}
}
And in my LoginView I do the following:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
// Manipulate navigation stack
NavigationController.SetViewControllers(
NavigationController.ViewControllers.Where(
viewController => viewController is LoginView).ToArray(), false);
ScannerView.MetadataFound += (s, e) =>
{
Console.WriteLine($"Found: [{e.Type.ToString()}] {e.StringValue}");
LoginViewModel.BarCode = e.StringValue;
if (LoginViewModel.DoneCommand.CanExecute())
{
ScannerView.Session.StopRunning();
LoginViewModel.DoneCommand.Execute();
}
};
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
ScannerView.Session.StartRunning();
ScannerView.SetMetadataType(AVMetadataObjectType.QRCode | AVMetadataObjectType.EAN13Code);
}
Funny thing is, that this works once I logged in with the manual input and logged out again, so I'm on the same screen again (possibly not the same but a new instance of it as the GC may destroy the view as it is removed from the navigation stack?)
I have put the scannerview as a subview on the LoginView in the storyboard. For navigation I use MVVMCross. (just for info)
So: What am I doing wrong? What do I need to do to make it work on the first load? (I got it to do that once - with the same code... maybe it is a timing issue?)
Obviously this is a timing issue.
I solved it by adding a "Tap to scan" paradigm.
When tapping I execute the following code:
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
Console.WriteLine($"Current types to scan: {this.MetadataOutput.MetadataObjectTypes}");
this.SetMetadataType(this.MetadataObjectType);
Console.WriteLine($"New types to scan: {this.MetadataOutput.MetadataObjectTypes}");
}
public void SetMetadataType(AVMetadataObjectType type)
{
//Confusing! *After* adding to session, tell output what to recognize...
this.Session.BeginConfiguration();
this.MetadataOutput.MetadataObjectTypes = type;
this.Session.CommitConfiguration();
}
Where MetadataObjectType is set to the codes we're looking for before.
And that solves the problem - the scanning now works every time.
I think the magic part is the Begin- and CommitConfiguration call, as this also works, if I do not use the touch to scan paradigm.

MvvmLight unable to create a controller for key

I am designing a cross platform application architecture using Xamarin iOS and Xamarin Android I decided to go with MvvmLight, it looks descent and is not hiding everything from the MVVM pattern, good and flexible.
While everything started to make sense trying to set it up and learn how to use it, I find myself difficult to understand why I get the following error.
Unable to create a controller for key ChartsPage
The setup.
In a PCL I have my ViewModels. I have a ViewModelLocator setup. I use the mvvmlightlibs Nuget Package.
public class ViewModelLocator
{
public static readonly string SchedulerPageKey = #"SchedulerPage";
public static readonly string ChartsPageKey = #"ChartsPage";
[SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public SchedulerViewModel Scheduler
{
get
{
return ServiceLocator.Current.GetInstance<SchedulerViewModel>();
}
}
public BizchartsViewModel Bizcharts
{
get
{
return ServiceLocator.Current.GetInstance<BizchartsViewModel>();
}
}
static ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
if (ViewModelBase.IsInDesignModeStatic)
{
// Haven't declared something yet
}
else
{
// Haven't declared something yet
}
SimpleIoc.Default.Register<SchedulerViewModel>();
SimpleIoc.Default.Register<BizchartsViewModel>();
}
}
The I have a unified iOS application using universal storyboard with size classes which has an initial UINavigationViewController SchedulerViewController and in the ViewDidLoad method I test the navigation to BizchartsViewController with 3 seconds delay. After 3 seconds I get the exceptions.
In the AppDelegate.
private static ViewModelLocator _locator;
public static ViewModelLocator Locator
{
get
{
if (_locator == null)
{
SimpleIoc.Default.Register<IDialogService, DialogService>();
_locator = new ViewModelLocator();
}
return _locator;
}
}
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
var nav = new NavigationService();
nav.Initialize((UINavigationController)Window.RootViewController);
nav.Configure(ViewModelLocator.ChartsPageKey, typeof(BizchartsViewController));
SimpleIoc.Default.Register<INavigationService>(() => nav);
return true;
}
The SchedulerViewController.
partial class SchedulerViewController : UIViewController
{
public SchedulerViewModel Vm {
get;
private set;
}
public SchedulerViewController (IntPtr handle) : base (handle)
{
Vm = AppDelegate.Locator.Scheduler;
}
public async override void ViewDidLoad ()
{
base.ViewDidLoad ();
await Task.Delay (3000);
Vm.NavigateToCharts ();
}
}
The SchedulerViewModel.
public class SchedulerViewModel : ViewModelBase
{
public void NavigateToCharts()
{
var nav = ServiceLocator.Current.GetInstance<INavigationService>();
nav.NavigateTo(ViewModelLocator.ChartsPageKey);
}
}
I definitely miss a detail somewhere!!!
If you follow carefully the blog post here, it says that with Storyboard you should use the string overload and not the typeof() in nav.Configure(Key, ViewController) and always set the storyboardId and restorationId in the Storyboard ViewController.
Note that because we are using a Storyboard, you must make sure to use
the Configure(string, string) overload, and NOT the Configure(string,
Type) one.

MvvmCross ViewModel transition from the left

I am developing an app for iOS using MvvmCross. On one of my Views I have some basic report data that is displayed in a tableview.
When the table row is touched a new view containing a detail report is displayed by making the call to ShowViewModel passing some parameters in a Dictionary. This works fine.
When the user swipes left or right the app needs to show the detail report for the next or previous item in the original list. I am doing this by updating some parameters and calling ShowViewModel again. The logic behind this is all working fine.
My problem; ShowViewModel animates the new view coming in from the right. This is perfect when the user has swiped left. However when swiping right it seems counter intuitive. How can I make ShowViewModel animate or transition in from the left side?
if you look to the MvvmCross source code here you see how the default behavior is showing the ViewControllers
You need to change that by doing something like the following:
How to change the Push and Pop animations in a navigation based app
for that, one idea is to have a custom view presenter and catch navigation to that particular view-model (override Show(IMvxTouchView view) )
or, maybe derive from UINavigationController, set it to MvvmCross to use it (look to the MvxSetup), and on some events change transition to that particular view
similar to this question
How to specify view transitions on iPhone
This is the solution I was able to come up with following the helpful pointers in the answer from Andrei N. In the end I opted for a TransitionFlipFromRight and TransitionFlipFromLeft when scrolling between detail reports. Hopefully it is useful to somebody else.
I already had a presenter class that was inherited from MvxModalSupportTouchViewPresenter
public class BedfordViewPresenter : MvxModalSupportTouchViewPresenter
Within this class I added a property of MvxPresentationHint.
private MvxPresentationHint _presentationHint;
In the override of method ChangePresentation the above property is used to store the passed in parameter
public override void ChangePresentation (MvxPresentationHint hint)
{
_presentationHint = hint;
base.ChangePresentation (hint);
}
Two new MvxPresentationHint class were declared (see later)
In the presenter class the Show method was overridden
public override void Show(IMvxTouchView view)
{
if (_presentationHint is FlipFromRightPresentationHint) {
var viewController = view as UIViewController;
MasterNavigationController.PushControllerWithTransition (viewController, UIViewAnimationOptions.TransitionFlipFromRight);
}
else
if (_presentationHint is FlipFromLeftPresentationHint) {
var viewController = view as UIViewController;
MasterNavigationController.PushControllerWithTransition (viewController, UIViewAnimationOptions.TransitionFlipFromLeft);
}
else {
base.Show (view);
}
_presentationHint = null;
}
A new class that provides extensions to a UINavigationController was created with the method PushControllerWithTransition
public static class UINavigationControllerExtensions
{
public static void PushControllerWithTransition(this UINavigationController
target, UIViewController controllerToPush,
UIViewAnimationOptions transition)
{
UIView.Transition(target.View, 0.75d, transition, delegate() {
target.PushViewController(controllerToPush, false);
}, null);
}
}
All that needs to be defined now are the two new MvxPresentationHint class derivations. These belong in your Core class library project rather than the iOS application project.
public class FlipFromLeftPresentationHint : MvxPresentationHint
{
public FlipFromLeftPresentationHint ()
{
}
}
and
public class FlipFromRightPresentationHint: MvxPresentationHint
{
public FlipFromRightPresentationHint ()
{
}
}
I hope this is a help to someone else trying to do something similar
Share my solution for android:
On view:
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = base.OnCreateView(inflater, container, savedInstanceState);
var layout = view.FindViewById<LinearLayout>(Resource.Id.swippeable);
var swipeListener = new SwipeListener(this.Activity);
swipeListener.OnSwipeLeft += (sender, e) => this.ViewModel.LeftCommand?.Execute(); //Here use command into view model
swipeListener.OnSwipeRight += (sender, e) => this.ViewModel.RightCommand?.Execute();
layout.SetOnTouchListener(swipeListener);
return view;
}
Gesture listener:
public class SwipeListener : SimpleOnGestureListener, View.IOnTouchListener
{
private const int SWIPE_THRESHOLD = 100;
private const int SWIPE_VELOCITY_THRESHOLD = 100;
private readonly GestureDetector gestureDetector;
public SwipeListener(Context ctx)
{
this.gestureDetector = new GestureDetector(ctx, this);
}
public Boolean OnTouch(View v, MotionEvent e)
{
return this.gestureDetector.OnTouchEvent(e);
}
public event EventHandler OnSwipeRight;
public event EventHandler OnSwipeLeft;
public event EventHandler OnSwipeTop;
public event EventHandler OnSwipeBottom;
public override Boolean OnDown(MotionEvent e)
{
return true;
}
public override Boolean OnFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
{
Boolean result = false;
float diffY = e2.GetY() - e1.GetY();
float diffX = e2.GetX() - e1.GetX();
if (Math.Abs(diffX) > Math.Abs(diffY))
{
if (Math.Abs(diffX) > SWIPE_THRESHOLD && Math.Abs(velocityX) > SWIPE_VELOCITY_THRESHOLD)
{
if (diffX > 0)
{
SwipeRight();
}
else
{
SwipeLeft();
}
result = true;
}
}
else if (Math.Abs(diffY) > SWIPE_THRESHOLD && Math.Abs(velocityY) > SWIPE_VELOCITY_THRESHOLD)
{
if (diffY > 0)
{
SwipeBottom();
}
else
{
SwipeTop();
}
result = true;
}
return result;
}
public void SwipeRight()
{
this.OnSwipeRight?.Invoke(this, EventArgs.Empty);
}
public void SwipeLeft()
{
this.OnSwipeLeft?.Invoke(this, EventArgs.Empty);
}
public void SwipeTop()
{
this.OnSwipeTop?.Invoke(this, EventArgs.Empty);
}
public void SwipeBottom()
{
this.OnSwipeBottom?.Invoke(this, EventArgs.Empty);
}
}

Resources