In a cross platform Xamarin app built with the MvvmCross framework I'm using a ToggleButton Widget in an Android .axml layout. I've bound the Checked property to a View Model property using a converter using the following binding syntax:
Checked MarketBuySellViewModel.Direction, Converter=DirectionBool, ConverterParameter='Sell'
Everything works well. On the iOS side, it appears you can use UIButton as a ToggleButton by using the Selected property. This implies that the following binding should achieve what I want on iOS:
set.Bind (SellButton).For(b => b.Selected).To (vm => vm.MarketBuySellViewModel.Direction).WithConversion("DirectionBool", "Sell");
I don't get any binding errors in the application output but the binding itself doesn't seem to work. Clicking the button doesn't set the Direction property and setting the direction to a different value does not set the Selected property on the UIButton.
Do I need to create a Custom Binding or am I simply setting up the binding incorrectly?
I also tried using a UISegmentedControl to achieve the same effect. Is binding to this control supported at all in MvvmCross? I don't see any reference to it in the source code. Does this mean I need to create custom bindings for it too?
For the UIButton, I don't believe there's any included Selected binding built into MvvmCross. Because of this - and because Selected doesn't have a simple paired event SelectedChanged, then I believe Selected binding should work one-way (from ViewModel to View) but not two-way.
There is a binding for the On of a UISwitch control and that's the control I've seen used most in these situations.
If you wanted to add a custom 2-way binding for Selected then I guess you'd have to do this using the ValueChanged event (but would need to check that is correct).
To do so, you'd just build a target binding something like:
public class MvxUIButtonSelectedTargetBinding : MvxPropertyInfoTargetBinding<UIButton>
{
public MvxUIButtonSelectedTargetBinding(object target, PropertyInfo targetPropertyInfo)
: base(target, targetPropertyInfo)
{
var view = View;
view.ValueChanged += HandleValueChanged;
}
private void HandleValueChanged(object sender, System.EventArgs e)
{
var view = View;
if (view == null)
return;
FireValueChanged(view.Selected);
}
public override MvxBindingMode DefaultMode
{
get { return MvxBindingMode.TwoWay; }
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
var view = View;
if (view != null)
{
view.ValueChanged -= HandleValueChanged;
}
}
}
}
and this could be registered in Setup in protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry) using something like:
registry.RegisterPropertyInfoBindingFactory(typeof(MvxUIButtonSelectedTargetBinding), typeof(UIButton),
"Selected");
Similarly, I don't believe anyone has added a two way UISegmentedControl binding yet - but would happily see one added.
Building a two way UISegmentedControl binding would be quite straight-forward - you'd just have to bind to the pair SelectedSegment and ValueChanged - with code similar to above.
Alternatively, you could switch to using a custom MySegmentedControl which had a nicer Value`ValueChanged` pair which would automatically work without a custom binding - e.g.:
public class MySegmentedControl : UISegmentedControl
{
// add more constructors if required
public int Value
{
get { return base.SelectedSegment; }
set { base.SelectedSegment = value; }
}
}
If any or all of these custom bindings are needed, then the Mvx project is happy to get these bindings added as issues or pull requests along with test/demo UIs in the https://github.com/slodge/MvvmCross-Tutorials/blob/master/ApiExamples/ApiExamples.Touch/Views/FirstView.cs project
Could be helpful to someone else, so i'm sharing my experience. I needed a two way binding for UISegmentedControl.SelectedSegment property to a ViewModel. The one way biding (ViewModel => View) works by default. I couldn't able to properly utilize the solution proposed by Stuart - to subclass the UISegmentedControl. I tried to ensure that the linker does not rip off the new custom control code, but this didn't help me a bit. So a perfectly viable solution is the one with MvxPropertyInfoTargetBinding. Here is the code working ok for me:
public class MvxUISegmentedControlSelectedSegmentTargetBinding : MvxPropertyInfoTargetBinding<UISegmentedControl>
{
public MvxUISegmentedControlSelectedSegmentTargetBinding(object target, PropertyInfo targetPropertyInfo)
: base(target, targetPropertyInfo)
{
this.View.ValueChanged += HandleValueChanged;
}
private void HandleValueChanged(object sender, System.EventArgs e)
{
var view = this.View;
if (view == null)
{
return;
}
FireValueChanged(view.SelectedSegment);
}
public override MvxBindingMode DefaultMode
{
get { return MvxBindingMode.TwoWay; }
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
var view = this.View;
if (view != null)
{
view.ValueChanged -= HandleValueChanged;
}
}
}
}
public class Setup : MvxTouchSetup
{
...
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
registry.RegisterPropertyInfoBindingFactory(typeof(MvxUISegmentedControlSelectedSegmentTargetBinding), typeof(UISegmentedControl), "SelectedSegment");
}
}
Related
I want to use a third party view controller that already inherits from UIViewController (https://bitbucket.org/thedillonb/monotouch.slideoutnavigation/src/f4e51488598b/MonoTouch.SlideoutNavigation?at=master), how would I integrate that with MVVMCross?
I could just take the source and change it to inherit from MvxViewController, but guessing I will run into this with other libraries.
Do I need to implement all the interfaces MvxViewController does? IMvxTouchView? IMvxEventSourceViewController?
For this particular case, where you don't actually want to do any data-binding so you can just use a custom presenter - e.g. see #Blounty's answer, or see this project demo - https://github.com/fcaico/MvxSlidingPanels.Touch
If you ever do need to convert third party ViewController base classes so that they support data-binding, then the easiest way is exactly what you guessed:
inherit from them to provide an EventSource-ViewController
inherit from the EventSource-ViewController to add the Mvx BindingContext
This technique is exactly how MvvmCross itself extends each of UIViewController, UITableViewController, UITabBarController, etc in order to provide data-binding.
For example, see:
extending UIViewController to provide an eventsource - MvxEventSourceViewController.cs
extending the event source ViewController to provide a binding context - MvxViewController.cs
Note that because C# doesn't have any Multiple-Inhertiance or any true Mixin support, this adaption of ViewControllers does involve a little cut-and-paste, but we have tried to minimise this through the use of event hooks and extension methods.
If it helps, this iOS technique for a previous MvvmCross version was discussed in Integrating Google Mobile Analytics with MVVMCross (obviously this is out of date now - but the general principles kind of remain the same - we adapt an existing viewcontroller via inheritance)
In Android, a similar process is also followed for Activity base classes - see ActionBarSherlock with latest MVVMCross
You can use a custom view presenter like below, This is pretty much straight out of my app using the SlideOutNavigation.
public class Presenter
: IMvxTouchViewPresenter
{
private readonly MvxApplicationDelegate applicationDelegate;
private readonly UIWindow window;
private SlideoutNavigationController slideNavigationController;
private IMvxTouchViewCreator viewCreator;
public Presenter(MvxApplicationDelegate applicationDelegate, UIWindow window)
{
this.applicationDelegate = applicationDelegate;
this.window = window;
this.slideNavigationController = new SlideoutNavigationController();
this.slideNavigationController.SlideWidth = 200f;
this.window.RootViewController = this.slideNavigationController;
}
public async void Show(MvxViewModelRequest request)
{
var creator = Mvx.Resolve<IMvxTouchViewCreator>();
if (this.slideNavigationController.MenuView == null)
{
// TODO: MAke this not be sucky
this.slideNavigationController.MenuView = (MenuView)creator.CreateView(new MenuViewModel());
((MenuView) this.slideNavigationController.MenuView).MenuItemSelectedAction = this.MenuItemSelected;
}
var view = creator.CreateView(request);
this.slideNavigationController.TopView = (UIViewController)view;
}
public void ChangePresentation(MvxPresentationHint hint)
{
Console.WriteLine("Change Presentation Requested");
}
public bool PresentModalViewController(UIViewController controller, bool animated)
{
Console.WriteLine("Present View Controller Requested");
return true;
}
public void NativeModalViewControllerDisappearedOnItsOwn()
{
Console.WriteLine("NativeModalViewControllerDisappearedOnItsOwn");
}
private void MenuItemSelected(string targetType, string objectId)
{
var type = Type.GetType(string.Format("App.Core.ViewModels.{0}ViewModel, AppCore", targetType));
var parameters = new Dictionary<string, string>();
parameters.Add("objectId", objectId);
this.Show(new MvxViewModelRequest { ViewModelType = type, ParameterValues = parameters });
}
}
I've an UINavigationViewController with an UISegmentedControl in the navigation bar. I want to achieve a simple switching to different ViewControllers when users push on the segmented control.
I've tried a lot and nothing works... Considered sources:
MonoTouch Instantiating a ViewController programmatically for ContainerView
https://stackoverflow.com/search?q=viewcontroller+intptr+handle
And a lot of google research...
The whole project is storyboard based! Any solutions which targets NIB's aren't useful.
Adding a ContainerControl to my UINavigationViewController. But in this case I can only embed one controller. Creating a Embedded-Segue programmatically was not possible. Even more instantiating a UITableViewController in code which is designed in IB results in an empty view. Because I've to change the c'tor from MyTableViewController(IntPtr handle) : base(handle) to an empty constructor.
Can someone publish a working example how to use a UISegmentedControl to switch between different ViewControllers? I appreciate all your help very much.
Working solution:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
CreateAndEmbed (TrDetailNavType.Info);
}
partial void segmentNavigationValueChanged (MonoTouch.UIKit.UISegmentedControl sender, MonoTouch.UIKit.UIEvent e)
{
CreateAndEmbed((TrDetailNavType)sender.SelectedSegment);
}
private void CreateAndEmbed(TrDetailNavType tab)
{
if (_currentViewController != null)
{
_currentViewController.View.RemoveFromSuperview ();
_currentViewController.RemoveFromParentViewController();
}
string id;
switch (tab)
{
case TrDetailNavType.Info:
id = "TagesRapportDetailInfoTableViewController";
break;
case TrDetailNavType.Lohn:
case TrDetailNavType.Material:
case TrDetailNavType.Inventar:
case TrDetailNavType.Fremdleistung:
case TrDetailNavType.Regie:
id = "TagesRapportDetailDummyViewController";
break;
}
_currentViewController = (UIViewController)Storyboard.InstantiateViewController (id);
_currentViewController.View.Frame = containerDetail.Bounds;
AddChildViewController (_currentViewController);
_currentViewController.DidMoveToParentViewController (this);
containerDetail.AddSubview (_currentViewController.View);
}
I need a custom binding and I know when and where but I don't know how I can do it. This is the relation of the view in my custom binding. Think about the *Views like controls.
I have the connections from ViewModel->ContainerView->FirstView but I can't connect it with the TableView. To connect the ContainerView to FirstView I did a custom binding (in one direction for now). And in the setvalue method I call the firstview's method SetBinding (where I want to do the binding)
I tried a few option but nothing happens, the last one looks like this:
public GolferList CurrentGolferList { get; set; }
public void SetBinding(GolferList golferList){
this.CurrentGolferList = golferList;
TableSource = new TableSourcePlayers(TableViewPlayers);
var bindingDescription = new[]{
new MvxBindingDescription {TargetName = "ItemsSource",SourcePropertyPath = "CurrentGolferList"} ,
};
Binder.Bind(this,TableSource, bindingDescription);
TableViewPlayers.Source = TableSource;
TableViewPlayers.ReloadData();
}
I would be grateful if you could tell me another way to handle it.
Update:
I followed Stuart's link and now it works fine, thanks a lot Stuart!
Actually, in my scheme the TableView is a MvxSimpleBindableTableViewSource and I want to bind the data there. So in order to make it work, I used the code below (SetBinding needs some external refactor):
private List<IMvxUpdateableBinding> bindings;
private string BindingText = "{'ItemsSource':{'Path':'CurrentGolfers'}}";
public object DataContext {
get { return dataContext; }
set { dataContext = value;
if (bindings == null)
bindings = this.GetService<IMvxBinder>().Bind(dataContext, TableSource, BindingText).ToList();
else
bindings.ForEach(b => b.DataContext = dataContext);
}
}
public void SetBinding(GolferList golferList){
this.DataContext = PlayViewModel;
tableView.Source = TableSource;
tableView.ReloadData();
}
Note that BindingText points to the table, not to the view itself.
Update 2
Now in V3 it's a bit different. First, the view must implement IMvxBindable and this members:
public object DataContext
{
get { return BindingContext.DataContext; }
set { BindingContext.DataContext = value; }
}
public IMvxBindingContext BindingContext { get; set; }
(Don't forget dispose calling BindingContext.ClearAllBindings() and also call to CreateBindingContext() in the viewload )
And then you'll be able to bind in your class. In my case:
var set = this.CreateBindingSet<FirstPlayViewController, PlayViewModel>();
set.Bind(source).To(vm => vm.CurrentGolfers).Apply(); //I love the new fluent api :)
I think what you want to do is actual a data-bound View, rather than a custom binding.
This is covered in this question - Custom bindable control in a MvvmCross Touch project
Basically what you need to do is to add a collection of 'Bindings' and the 'DataContext' property to your FirstView.
If you do that then you should be able to databind (to DataContext) within FirstView just like you do within any normal MvvmCross view.
Note - this will be much easier to do in v3 as we've added a 'BindingContext' object to assist with exactly this type of operation
I have an iPhone app that supports all orientations but in one UIViewController I only want to support portrait (or upside down).
I have added the following code to my UIViewController but it still rotates.
public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)
{
// Return true for supported orientations
return ((toInterfaceOrientation != UIInterfaceOrientation.LandscapeLeft) && (toInterfaceOrientation != UIInterfaceOrientation.LandscapeRight));
}
No matter where I add the ShouldAutorotateToInterfaceOrientation code it still rotates!
Is there a way to allow the app to support all orientations for all UIViewControllers except one?
Also i am using a NavigationController - does that affect things?
if you use iOS 6 you must override GetSupportedInterfaceOrientations method instead ShouldAutorotateToInterfaceOrientation
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations()
{ ... }
Probably this happens because your root navigation controller brings it own rotation. Lets assume that your root navigation controller is a normal UINavigationViewController. Create a derived own version:
public class UINavigationControllerWithoutRotation : UINavigationController
{
public UINavigationControllerWithoutRotation()
{
}
public UINavigationControllerWithoutRotation(UIViewController viewController) : base(viewController)
{
}
public override bool ShouldAutorotate()
{
return false;
}
}
Now use this as the root controller. Find the code saying something like:
var myRootController = new UINavigationController();
and replace it with
var myRootController = new UINavigationControllerWithoutRotation();
That should do the job. Keep in mind, that you have to do it a little bit different if for instance your root view controller is a UITabBarController!
I have QLPreviewController up and running but I'm using PresentModalViewController() to show the QLPreviewController directly. For reasons beyond explanation, I would like to have my own UIViewController which will create its own view and within that view I would like to use the QLPreviewController. Should be easy I thought, but the code below just does nothing. The QLPreviewControllers ViewDidAppear never gets called. (In my example below, PreviewController inherits from QLPreviewController and encapsulates delegate, preview item and source).
Can somebody explain what is wrong with the code below (besides the fact that it is pointless :-))?
Oh, yeah: in my test scenario, I present the controller below modally. It shows up but witout the preview.
public class OuterPreviewController : UIViewController
{
public OuterPreviewController (QLPreviewControllerDataSource oDataSource) : base()
{
this.oDataSource = oDataSource;
}
private PreviewController oPreviewController;
private QLPreviewControllerDataSource oDataSource;
public override void LoadView ()
{
this.View = new UIView();
this.View.Frame = new RectangleF(0, 0, 500, 500);
this.View.BackgroundColor = UIColor.Red;
}
public override void ViewDidAppear (bool animated)
{
// Code execution comes her. No errors, no issues.
base.ViewDidAppear (animated);
this.oPreviewController = new PreviewController();
this.oPreviewController.DataSource = this.oDataSource;
// Preview controller's view is added but it never shows up.
this.View.AddSubview(this.oPreviewController.View);
this.oPreviewController.View.Frame = this.View.Frame;
this.oPreviewController.View.Center = this.View.Center;
}
public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)
{
return true;
}
}
Found a solution by coincidence today: all ReloadData() on the preview controller and magically it will show its contents.
This allows to add a QLPreviewController to an existing view as a subview and embed a preview. It also gets you rid of the toolbar which contains the open in menu.