How to make my RefreshControl appear during ViewDidAppear - ios

I've answered this question myself - just leaving it here now for people with the same issue.
My code is fairly simple - A custom table with a datasource to provide data asynchronously from a web service.
In order to get the nicest user experience I would like to have the UIRefreshControl animate the process of loading whenever this controller appears, instead of just when the list has been pulled down.
Unfortunately the UIRefreshControl does not appear at all if I call my method during ViewDidAppear.
I've tried suggestions from answers these questions, but none of them seemed to work for me:
public partial class ControlCenterSelectionController : UITableViewController
{
public ControlCenterSelectionController (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
TableView.RefreshControl = new UIRefreshControl();
TableView.RefreshControl.ValueChanged += RefreshControlOnValueChanged;
}
private async void RefreshControlOnValueChanged(object sender, EventArgs eventArgs)
{
await UpdateDataSourceAsync();
}
public override async void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
TableView.SetContentOffset(new CGPoint(0, -TableView.RefreshControl.Frame.Size.Height), true);
await UpdateDataSourceAsync();
}
private async Task UpdateDataSourceAsync()
{
TableView.RefreshControl.BeginRefreshing();
var ccClient = DI.Instance.Get<IControlCenterRestClient>();
var controlCenters = await ccClient.GetAllAsync();
var source = new GenericViewSource<ControlCenter>(controlCenters, item => item.Name, ControlCenterSelected);
TableView.Source = source;
TableView.RefreshControl.EndRefreshing();
}
private void ControlCenterSelected(ControlCenter controlCenter)
{
var controller = this.InstantiateController(Constants.ViewControllerIds.ControlCenter) as ControlCenterController;
controller.ControlCenter = controlCenter;
this.NavigateTo(controller);
}
}
UITableView UIRefreshControl Does Not Show Its View The First Time
UIRefreshControl on viewDidLoad
UIRefreshControl - beginRefreshing not working when UITableViewController is inside UINavigationController
All I currently get is a screen which looks empty, loads data without indication and then updates the screen once it's done.
Can someone spot an error? I can't really find one, since this code is rather simple.

The desperation of posting it on SO + desperate coding attempts were key:
public override async void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
TableView.SetContentOffset(new CGPoint(0, -(TableView.RefreshControl.Frame.Size.Height + 20)), true);
TableView.RefreshControl.BeginRefreshing();
await UpdateDataSourceAsync();
}
private async Task UpdateDataSourceAsync()
{
var ccClient = DI.Instance.Get<IControlCenterRestClient>();
var controlCenters = await ccClient.GetAllAsync();
var source = new GenericViewSource<ControlCenter>(controlCenters, item => item.Name, ControlCenterSelected);
TableView.Source = source;
TableView.RefreshControl.EndRefreshing();
}
This change in particular made the difference:
-(TableView.RefreshControl.Frame.Size.Height + 20)
Turns out that, while in the other questions the issue was fixed by simply applying a scroll based on RefreshControl would fire the animation, in my case I had to add some extra y delta to make the animation fire.
Correction
this extra offset seems to be required only if extended edges is set to extend under top bars.

Related

Xamarin iOS: Kill off async event handlers when a view controller is removed from the navigation stack

My RootViewController is a UINavigationController. The navigation stack starts with ViewController which then pushes CustomViewController.
public class ViewController : UIViewController
{
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
View.BackgroundColor = UIColor.Red;
NavigationController.PushViewController(new CustomViewController(), true);
}
}
public class CustomViewController : UIViewController
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
View.BackgroundColor = UIColor.DarkGray;
var button = new UIButton
{
TranslatesAutoresizingMaskIntoConstraints = false
};
View.AddSubview(button);
button.SetTitle("Click me", UIControlState.Normal);
button.WidthAnchor.ConstraintEqualTo(250).Active = true;
button.HeightAnchor.ConstraintEqualTo(50).Active = true;
button.CenterXAnchor.ConstraintEqualTo(View.CenterXAnchor).Active = true;
button.CenterYAnchor.ConstraintEqualTo(View.CenterYAnchor).Active = true;
button.AddTarget(async (sender, e) =>
{
await Task.Delay(3000);
NavigationController.PushViewController(new CustomViewController(), true);
}, UIControlEvent.TouchUpInside);
}
}
The problem I'm having is when the user clicks the button and pops its containing view controller immediately afterwards, the async event handler keeps running. After 3 seconds, it will attempt to push the same view controller, but NavigationController is set to null.
When a page is dismissed / popped off the stack, is there a way to cancel all running async methods? Note that my actual use case involves more complex interactions. I tried using a CancellationToken (ThrowIfCancellationRequested()) which helps with REST calls, but it is still possible that the user might go back to the previous page after the API call is done and before the async handler finishes.
I'm trying to find a generic solution to this because my app will eventually have dozens of async event handlers.

UITableViewDelegate disables itemselected

I am trying to make a simple change to a tableview for IOS. As my project is using Xamarin forms I use a custom renderer.
The changes I want to make is to close the keyboard is the table is scrolled, simple as that.
When I make the following changes it will work for my new scrolled override event but stops item (cell) selected from triggering (some of my cells navigate away from the page) and must be tapped.
public class CustomTableRenderer : TableViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
{
base.OnElementChanged(e);
if (Control == null)
return;
var tableView = Control as UITableView;
TableDelegate tableDelegate;
tableDelegate = new TableDelegate();
tableView.Delegate = tableDelegate;
}
}
public class TableDelegate : UITableViewDelegate
{
#region Constructors
public TableDelegate()
{
}
public TableDelegate(IntPtr handle) : base(handle)
{
}
public TableDelegate(NSObjectFlag t) : base(t)
{
}
#endregion
#region Override Methods
public override void Scrolled(UIScrollView scrollView)
{
UIApplication.SharedApplication.KeyWindow.EndEditing(true);
//base.Scrolled(scrollView);
}
#endregion
}
I have tried changing the UITableViewDelegate to UITableViewController but seems to cause more problems.
Adding in the override for all other methods calling base also does not seem to help. Not sure what I am missing.
Any idea why this is caused?
Note: If more information is needed please comment to let me know. Same with if the question is not clear. Comments would help.

Custom iOS UIView Class MvvmCross Constraints - Draw(CGRect rect) Method Never Called

I have a situation that I cannot seem to work out Where the specific problem lies and I'm hoping for a little input.
I have a very simple view that at the moment just has a UIScrollView. In the ViewDidLoad method I register to listen for the view models PropertyChanged event like this:
public override void ViewDidLoad()
{
base.ViewDidLoad();
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "MyCollection")
{
BuildUi();
}
}
When that event is fired it kicks off building the view since the data is now available within the view model. In the BuildUi method I have this code:
View Code
private void BuildUi()
{
_mainScrollView = new UIScrollView(View.Frame)
{
ShowsHorizontalScrollIndicator = false,
AutoresizingMask = UIViewAutoresizing.All
};
Add(_mainScrollView);
View.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
View.AddConstraints(
_mainScrollView.AtLeftOf(View),
_mainScrollView.AtTopOf(View),
_mainScrollView.WithSameWidth(View),
_mainScrollView.WithSameHeight(View));
foreach(var ctx in ViewModel.MyCollection)
{
var contextScoreControl = new ContextScoreView(new CGRect(100,100,100,100), ctx);
_mainScrollView.Add(contextScoreControl);
//var btn = ViewHelpers.CreateDefaultButton("test");
//_mainScrollView.Add(btn);
}
_mainScrollView.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
var constraints = _mainScrollView.VerticalStackPanelConstraints(new Margins(20, 10, 20, 10, 5, 5), _mainScrollView.Subviews);
_mainScrollView.AddConstraints(constraints);
}
Control Code
[Register("MyControl")]
public class MyControl : UIView
{
private MyViewModel _vm;
private UILabel _ctxTitle;
private UITextView _explanationText;
private UISlider _scoreSlider;
public MyControl(MyViewModel vm)
{
_vm = vm;
BackgroundColor = UIColor.Cyan;
}
public MyControl(IntPtr handle) : base(handle) { }
public MyControl(CGRect rect, MyViewModel vm)
{
_vm = vm;
BackgroundColor = UIColor.Cyan;
}
public MyControl(CGRect rect) : base(rect)
{
BackgroundColor = UIColor.Cyan;
}
public override void Draw(CGRect rect)
{
base.Draw(rect);
_ctxTitle = ViewHelpers.CreateTitleLabel();
_ctxTitle.Text = _vm.Text;
_explanationText = ViewHelpers.CreateAutoExpandingReadOnlyTextView();
_explanationText.Text = _vm.ScoringHint;
_scoreSlider = ViewHelpers.CreateDefaultSlider();
_scoreSlider.Value = _vm.Score;
AddSubview(_ctxTitle);
AddSubview(_explanationText);
AddSubview(_scoreSlider);
this.AddConstraints(
_ctxTitle.AtTopOf(this, IosConstants.UIPadding),
_ctxTitle.AtLeftOf(this, IosConstants.UIPadding),
_ctxTitle.WithSameWidth(this).Minus(IosConstants.UIPadding * 2),
_explanationText.Below(_ctxTitle, IosConstants.UIPadding),
_explanationText.WithSameLeft(_ctxTitle),
_explanationText.WithSameRight(_ctxTitle),
_scoreSlider.Below(_explanationText, IosConstants.UIPadding),
_scoreSlider.WithSameLeft(_explanationText),
_scoreSlider.WithSameRight(_explanationText),
_scoreSlider.AtBottomOf(this)
);
}
}
The issue I'm seeing right now is that the Draw method is never called which means nothing appears in the view.
I've tried lots and lots of permutations of the constructors you can see in the control code. Using CGRect.Empty and as is in the code a manually generated CGRect but no matter what I try I cannot seem to get this flow of code to work.
As you can see from the code if I swap out my custom control for a simple UIButton everything works as expected so the main view code where the UIScrollView is added with constraints is working and laying itself out as expected and desired. The problem seem to lie either in the control code itself or the hierarchy of the constraints themselves. I just cannot figure out what needs to happen to make this work.
Under certain circumstances I was seeing the following error:
Unable to simultaneously satisfy constraints.
I've used this "pattern" of code many times in other views within the app and it works in those situations which is why I'm slightly confused about what is going on in this situation. The difference is the looping over a dynamic number of controls in this situation.
Without changing the current structure of the view (to say using a UITableView instead) how can I get this playing nicely and render the custom UIView controls?

UIScrollView doesn't register events until ViewController Segue

very first SO question after long-time lurking. I've been teaching myself iOS development using C# under Xamarin with Visual Studio.
My problem statement is: Why does my scrollview only work, i.e. register events such as dragging, AFTER having used the ViewController segue to another view and back? While at the same time using scrolling through the PageControl works just fine at any time?
I've successfully used a wide range of sample code found both here, on the Xamarin site, and elsewhere (including some samples written in Swift which I translated to C# - Objective-C might as well be Minoan Linear B).
This also worked without issues in all variations until I started adding AutoLayout into the equation. Using FluentLayout made this a lot easier and I now overall have a well-behaved UI exactly the way I want it on all iPhone models.
The essential boilerplate code worked fine when it was all still all jammed into the root viewcontroller's ViewDidLoad, but now that I've broken it all up into "clean" classes it's showing this strange behaviour.
One problem I had to tackle was the problem of getting images to properly resize inside the UIImageView frame, when the size isn't available until the layout constraints are complete. As I now understand the concept, that's what ViewDidLayoutSubViews() is for.
Thus my root VS code:
namespace ScrollTest.iOS
{
public partial class RootViewController : UIViewController
{
...
public override void ViewDidLoad()
{
base.ViewDidLoad();
View = new MainView();
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
CGRect pageFrame = SharedStatic.ScrollView.Frame;
SharedStatic.ScrollView.ContentSize = new CGSize(pageFrame.Width * 2, pageFrame.Height);
SharedStatic.ImageView.Frame = pageFrame;
SharedStatic.ImageView.Image = UIImage.FromFile("toothless.jpg").Scale(pageFrame.Size);
SharedStatic.ImageView.ContentMode = UIViewContentMode.ScaleAspectFit;
pageFrame.X += SharedStatic.ScrollView.Frame.Width;
SharedStatic.TableView.Frame = pageFrame;
SharedStatic.TableView.ContentMode = UIViewContentMode.ScaleAspectFit;
}
...
}
From the MainView() class on downward I have a hierarchy of Views, which are instantiated and then set layout constraints, such as like this:
namespace ScrollTest.iOS
{
[Register("MainView")]
internal class MainView : UIView
{
public MainView()
{
Initialise();
}
public MainView(CGRect frame) : base(frame)
{
Initialise();
}
private void Initialise()
{
SharedStatic.MainView = this;
var navView = new NavView();
Add(navView);
var contentView = new ContentView();
Add(contentView);
var pagerView = new PagerView();
Add(pagerView);
var toolView = new ToolView();
Add(toolView);
var buttonView = new ButtonView();
Add(buttonView);
const int padding = 1;
var navHeight = 32;
var pageControlHeight = 25;
var toolHeight = 25;
var buttonHeight = 20;
this.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
this.AddConstraints
(
... other view constraints...
contentView.Below(navView, padding),
contentView.AtLeftOf(this),
contentView.WithSameWidth(this),
contentView.Bottom().EqualTo().TopOf(pagerView),
pagerView.Above(toolView, padding),
pagerView.AtLeftOf(this),
pagerView.WithSameWidth(this),
pagerView.Height().EqualTo(pageControlHeight),
...
);
}
}
}
The actual ScrollView class looks like this, with event handlers just hooked up to debug output for this test.
namespace ScrollTest.iOS
{
[Register("ContentView")]
internal class ContentView : UIScrollView
{
private ContainerView _containerView;
public ContentView()
{
Initialise();
}
public ContentView(CGRect frame) : base(frame)
{
Initialise();
}
private void Initialise()
{
SharedStatic.ScrollView = this;
_containerView = ContainerView.Instance;
Add(_containerView);
PagingEnabled = true;
ScrollEnabled = true;
Bounces = false;
DirectionalLockEnabled = true;
DecelerationEnded += scrollView_DecelerationEnded;
Scrolled += delegate { Console.WriteLine("scrolled"); };
DecelerationStarted += delegate { Console.WriteLine("deceleration started"); };
DidZoom += delegate { Console.WriteLine("did zoon"); };
DraggingEnded += delegate { Console.WriteLine("dragging ended"); };
DraggingStarted += delegate { Console.WriteLine("dragging started"); };
ScrollAnimationEnded += delegate { Console.WriteLine("Scroll animation ended"); };
ScrolledToTop += delegate { Console.WriteLine("Scrolled to top"); };
WillEndDragging += delegate { Console.WriteLine("will end dragging"); };
ZoomingEnded += delegate { Console.WriteLine("zooming ended"); };
ZoomingStarted += delegate { Console.WriteLine("zooming started"); };
this.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
this.AddConstraints
(
_containerView.AtLeftOf(this),
_containerView.AtRightOf(this),
_containerView.AtTopOf(this),
_containerView.AtBottomOf(this)
);
}
private void scrollView_DecelerationEnded(object sender, EventArgs e)
{
Console.WriteLine("Done changing page");
nfloat x1 = SharedStatic.ImageView.Frame.X;
nfloat x2 = SharedStatic.TableView.Frame.X;
nfloat x = this.ContentOffset.X;
if (x == x1)
{
Console.WriteLine("flip");
SharedStatic.PageControl.CurrentPage = 0;
}
else
{
Console.WriteLine("flop");
SharedStatic.PageControl.CurrentPage = 1;
}
}
}
}
And "following" it a container view which holds my image and table, which from my understanding of iOS coding practises is the way to do this (apart from the singleton class, which was the result of testing something else).
namespace ScrollTest.iOS
{
[Register("ContainerView")]
internal sealed class ContainerView : UIView
{
private UIImageView _imageView;
private UITableView _tableView;
private static readonly Lazy<ContainerView> lazy = new Lazy<ContainerView>(() => new ContainerView());
public static ContainerView Instance { get { return lazy.Value; } }
private ContainerView()
{
Initialise();
}
private ContainerView(CGRect frame) : base(frame)
{
Initialise();
}
private void Initialise()
{
SharedStatic.ContainerView = this;
Console.WriteLine("setting up scrolling stuff");
_imageView = new UIImageView();
Add(_imageView);
SharedStatic.ImageView = _imageView;
_tableView = new UITableView();
Add(_tableView);
SharedStatic.TableView = _tableView;
this.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
this.AddConstraints
(
_imageView.AtLeftOf(this),
_imageView.AtTopOf(this),
_imageView.AtBottomOf(this),
_imageView.WithSameWidth(this),
_tableView.Left().EqualTo().RightOf(_imageView),
_tableView.WithSameTop(_imageView),
_tableView.WithSameBottom(_imageView),
_tableView.WithSameWidth(_imageView)
);
}
}
}
On top of my screen is a navigation bar with a hamburger button which will trigger a segue to another viewcontroller, which down the road is in charge of popping up an Options screen. It holds another hamburger button to dismiss itself and return to the main screen. This works without issues:
_navBar.SetItems(new UINavigationItem[] { new UINavigationItem { LeftBarButtonItem = new UIBarButtonItem(UIImage.FromFile("hamburger32x32.png"), UIBarButtonItemStyle.Plain, BringUpOptions) } }, false);
And the event handler:
internal async void BringUpOptions(object s, EventArgs e)
{
Console.WriteLine("Options clicked");
var board = UIStoryboard.FromName("MainStoryboard", null);
var optionController = board.InstantiateViewController("OptionsViewController");
optionController.ModalTransitionStyle = UIModalTransitionStyle.FlipHorizontal;
await this.Window.RootViewController.PresentViewControllerAsync(optionController, true);
}
I use the storyboard editor only for my two view controllers and the segue. All other controls/views are programmatically generated, as are the layouts.
Putting this all together, results in a correctly laid out user interface, a correctly working segue to the options screen, a correct dismissal/return.
Also working is clicking on the page control: flipping back and forth between the image and the table view.
However, this paging does not work when trying to drag the image view over to the table view! Not at app startup that is! Debugging shows that no events are caught.
Until I go to Options screen at least once and then back! Then everything works as expected: page control flip/flops and scrollview scrolls back and forth.
I'm at a total loss why this would be the case! What does that initial segue initialise that is apparently necessary for the scrollview to work? I've plastered my code with debug output and single stepped to distraction. I just can't find it. Could it be a bug in Xamarin?
Is there some setting or call I need to make from within the view classes that I'm simply not aware of because it's normally hidden when using the storyboard editors? I've been over Xamarin's UIScrollView class doc but nothing jumps out at me.
I thought that my somewhat awkward use of this shared static class could be an issue, but I don't otherwise know how to get layout information across the various classes (therefore sizing of images fails). My intention was to dive into this and remove the kludge once I had resolved this showstopper of mine:
namespace ScrollTest.iOS
{
internal static class SharedStatic
{
internal static MainView MainView { get; set; }
internal static UIPageControl PageControl { get; set; }
internal static ContentView ScrollView { get; set; }
internal static ContentView ContentView => ScrollView;
internal static ContainerView ContainerView { get; set; }
internal static UIImageView ImageView { get; set; }
internal static UITableView TableView { get; set; }
}
}
With the code being so close to working perfectly, I'm really thinking that I'm missing something fairly straight-forward. Some newbie error that one doesn't run into when using storyboards, but which is somehow biting me.
Thanks!
EDIT 20150828: After some further research I've added overrides for SendEvent for the UIApplication:
[Register("TestApp")]
class TestApp : UIApplication
{
public override void SendEvent(UIEvent uievent)
{
base.SendEvent(uievent);
Console.WriteLine("event hit");
}
}
And then changed Main.cs to:
public class Application
{
// This is the main entry point of the application.
static void Main(string[] args)
{
UIApplication.Main(args, "TestApp", "AppDelegate");
}
}
As a result I can indeed see that touches/drags/swipes are causing event hits, but that they aren't identified/categorised properly. Drilling into the debugger, I was able to see that the UIScrollView is receiving the "hits" but is simply doing nothing with them! Essentially it's ignoring them totally - until I've done that very first view controller segue, then all events fire correctly.
This is starting to smack of a bug, but I simply don't know enough to be certain that it's not somewhere in my code or actually in Xamarin.
It's taken me a lot of cursing and screaming but I have been able to resolve the issue, get newly cropped up follow up problems fixed and get the entire target UI design to work. I hope my solution is of help to others.
First, in the end it was absolutely mandatory that I got my head around every single detail of the oft-referred to Apple technotes on UIScrollView and Autolayout.
Apple Technical Note TN2154
It was hard because I don't know Objective-C, but by whittling away at my ignorance through going back and forth with my Xamarin C# code and the Apple sample code I was able to drill down and learn and really understand the interaction between a scrolling view and the auto-layout constraints. My reluctance to dive into this sine qua non may have contributed to the time I needed to solve this issue.
While there's a gazillion links out there showing samples, many use the storyboard, which actually made my life harder. YMMV. As before I decided to take the programmatic route and use the tremendously useful
Cirrious FluentLayout on Github
The core of my solution, in the end, was the use of overriding LayoutSubviews() for my scrollview and the sub-view container class to contain the setting of constraints (apart from some other refactoring to bring everything into a single class file, with use of "partial" for break up the code optically).
For the UIScrollView I use one embedded UIView for the container. The ctor only declares the class, but the layout of that container is dealt with in the LayoutSubviews() method!
[Register("ScrollView")]
internal sealed class ScrollView : UIScrollView
{
internal ContainerView Container { get; }
internal ScrollView()
{
Container = new ContainerView();
Add(Container);
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
this.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
this.AddConstraints
(
Container.AtTopOf(this),
Container.AtLeftOf(this),
Container.WithSameWidth(this),
Container.WithSameHeight(this)
);
}
}
I repeated this for the views contained in my 2 scrolling pages:
[Register("ContainerView")]
internal sealed class ContainerView : UIView
{
private UIImageView _arenaView;
internal UIImageView Arena => _arenaView;
private UIView _gridView;
internal UIView Grid => _gridView;
internal ContainerView()
{
_arenaView = new UIImageView();
Add(_arenaView);
_gridView = new UIView()
Add(_gridView);
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
// determine the size of the basic frame in the scrolling area
CGRect pageFrame = SharedStatic.ScrollView.Frame;
// set it's overall size to n times the width, where is number pages, in this case 2
SharedStatic.ScrollView.ContentSize = new CGSize(pageFrame.Width * 2, pageFrame.Height);
_arenaView.Frame = pageFrame;
_arenaView.Image = UIImage.FromFile("someimage.jpg");
// offset by the width to get to second page
pageFrame.X += SharedStatic.ScrollView.Frame.Width;
_gridView.Frame = pageFrame;
this.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
this.AddConstraints
(
_arenaView.WithSameTop(this),
_arenaView.WithSameBottom(this),
_arenaView.WithSameHeight(this),
_arenaView.WithSameWidth(this),
_gridView.Left().EqualTo().RightOf(_arenaView),
_gridView.WithSameTop(_arenaView),
_gridView.WithSameBottom(_arenaView),
_gridView.WithSameWidth(_arenaView)
);
}
The reason for this approach as I understand it, is because not until the layout of the various subviews happens have the various frames and sizes been determined. Which means that I have to layout the "dynamic" subviews for my image and data grid themselves within the layout of the superviews for the constraints to properly apply, while at the same time still receiving the eventsl.
My original approach to put it all into the root view controller's ViewDidLayoutSubViews() led to the wrong view receiving all touch input, or worse not properly registering the event delegates at all. To a certain extent this is still a case of "no idea why it works now, but who am I to complain?"
An issue that remains is the constant and repeated calls to LayoutSubviews(). Every (even partial) scroll, touch action, segue will call this method over and over, whether a change in layout is needed or not. I found one helpful tutorial, which I promptly can no longer find, which explained to keep state information and minimise the number of calls in a smart way and I'll experiment with that next.
Information of when LayoutSubviews is called can be found here:
When is the layoutSubviews method called?
and links within that thread.
The horrid bit of tight coupling through the kludgy SharedStatic class will also be replaced, but you may find it useful to use such a construct during the testing and debugging stage.
Hope this helps someone out there!

How to open custom dialog box / popup using Xamarin.Forms?

I am newbie to Xamarin.Forms and stuck with a situation where I want to open up a popup box with my control details [e.g. View Employee Details] on click of parent page.
How can I open custom dialog box / popup using Xamarin.Forms?
Any example code will be appreciated?
Thanks in advance!
If you still want to have your popup's code in its own Page you can set up some custom renderers along the following logic.
1. A ModalPage & corresponding renderer
public class ModalPage : ContentPage { }
public class ModalPageRenderer : PageRenderer {
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
this.View.BackgroundColor = UIColor.Clear;
this.ModalPresentationStyle = UIModalPresentationStyle.OverCurrentContext;
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
SetElementSize (new Size (View.Bounds.Width, View.Bounds.Height));
}
}
2. HostPage
public class ModalHostPage : ContentPage, IModalHost
{
#region IModalHost implementation
public Task DisplayPageModal(Page page)
{
var displayEvent = DisplayPageModalRequested;
Task completion = null;
if (displayEvent != null)
{
var eventArgs = new DisplayPageModalRequestedEventArgs(page);
displayEvent(this, eventArgs);
completion = eventArgs.DisplayingPageTask;
}
// If there is no task, just create a new completed one
return completion ?? Task.FromResult<object>(null);
}
#endregion
public event EventHandler<DisplayPageModalRequestedEventArgs> DisplayPageModalRequested;
public sealed class DisplayPageModalRequestedEventArgs : EventArgs
{
public Task DisplayingPageTask { get; set;}
public Page PageToDisplay { get; }
public DisplayPageModalRequestedEventArgs(Page modalPage)
{
PageToDisplay = modalPage;
}
}
}
3. HostPage renderer
public class ModalHostPageRenderer: PageRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if(e.OldElement as ModalHostPage != null)
{
var hostPage = (ModalHostPage)e.OldElement;
hostPage.DisplayPageModalRequested -= OnDisplayPageModalRequested;
}
if (e.NewElement as ModalHostPage != null)
{
var hostPage = (ModalHostPage)e.NewElement;
hostPage.DisplayPageModalRequested += OnDisplayPageModalRequested;
}
}
void OnDisplayPageModalRequested(object sender, ModalHostPage.DisplayPageModalRequestedEventArgs e)
{
e.PageToDisplay.Parent = this.Element;
var renderer = RendererFactory.GetRenderer (e.PageToDisplay);
e.DisplayingPageTask = this.PresentViewControllerAsync(renderer.ViewController, true);
}
}
Then it is as simple as calling
await ModalHost.DisplayPageModal(new PopUpPage());
from your host page or in this particular case from the ViewModel behind.
What Pete said about PushModalAsync / PopModalAsync still remains valid for this solution too (which in my opinion is not a disadvantage), but your popup would appear with transparent background.
The main advantage of this approach, in my opinion, is that you can have your popup XAML/code definition separate from the host page and reuse it on any other page where you wish to show that popup.
The general purpose of what you are trying to achieve can be accomplished by using the PushModalAsync and PopModalAsync methods of Xamarin.Forms Navigation object.
The chances are that this is good enough for what you are needing - However - this isn't truely modal. I will explain after a small code snippet:-
StackLayout objStackLayout = new StackLayout()
{
};
//
Button cmdButton_LaunchModalPage = new Button();
cmdButton_LaunchModalPage.Text = "Launch Modal Window";
objStackLayout.Children.Add(cmdButton_LaunchModalPage);
//
cmdButton_LaunchModalPage.Clicked += (async (o2, e2) =>
{
ContentPage objModalPage = new ContentPage();
objModalPage.Content = await CreatePageContent_Page2();
//
await Navigation.PushModalAsync(objModalPage);
//
// Code will get executed immediately here before the page is dismissed above.
});
//
return objStackLayout;
private async Task<StackLayout> CreatePageContent_Page2()
{
StackLayout objStackLayout = new StackLayout()
{
};
//
Button cmdButton_CloseModalPage = new Button();
cmdButton_CloseModalPage.Text = "Close";
objStackLayout.Children.Add(cmdButton_CloseModalPage);
//
cmdButton_CloseModalPage.Clicked += ((o2, e2) =>
{
this.Navigation.PopModalAsync();
});
//
return objStackLayout;
}
The problem with the above is that the
await Navigation.PushModalAsync(objModalPage);
will immediately return after the animation.
Although you can't interact with the previous page, as we are displaying a new NavigationPage with a Close button shown - the parent Navigation Page is still executing behind the scenes in parallel.
So if you had any timers or anything executing these still would get called unless you stopped those.
You could also use the TaskCompletionSource approach as outlined in the following post also How can I await modal form dismissal using Xamarin.Forms?.
Note - that although you can now await the 2nd page displaying and then when that page is dismissed allowing code execution to continue on the next line - this is still not truely a modal form. Again timers or anything executing still will get called on the parent page.
Update 1:-
To have the content appear over the top of existing content then simply include it on the current page, however make this section of content invisible until you need it.
If you use an outer container such like a Grid that supports multiple child controls in the same cell, then you will be able to achieve what you want.
You will also want to use something like a filled Box with transparency that will cover the entire page also, to control the visible, see through section, that surrounds your inner content section.
I followed above approach and found it impossible to run on iOS 7.
I found this library BTProgressHUD which you can modify and use.
I Use its methods by Dependency service.
Actual library for popups.
https://github.com/nicwise/BTProgressHUD
Following example uses BTProgressHUD library internally.
https://github.com/xximjasonxx/ScorePredictForms

Resources