Xamarin iOS Unwind Segue or DismissViewController programmatically and pass back data - c#

I have created a segue in my storyboard. It is a modal segue and I gave it an identifier of LoginSegue.
In my controller, I do:
PerformSegue("LoginSegue", this);
All is well. New controller comes up as expected.
To return to the calling view, I can use an unwind segue by wiring a button to the unwind segue in the calling controller. This works fine.
Or I can call (from code):
DismissViewController(true, null);
The thing is that I want to pass back some data.
The ParentViewController is null.
I do need to do some validation before I return to the calling view, so doing the auto unwind from a button is not an option.
[Action("UnwindToCaller:")]
public void UnwindToCaller(UIStoryboardSegue seque)
{
var loginViewController = (LoginViewController)seque.SourceViewController;
var data = loginViewController.getData();
Console.WriteLine("Unwind to Caller Here.");
}
I also cannot seem to find a way to give the unwind segue a storyboard Id.
If I could perform the unwind segue from code everything would be good, or if I could pass some data back with the DismissViewController that would be good too.
Again, this seems like a pretty common thing to do.

OK, found a solution. Hope this helps others.
So in my calling view controller, when I PrepareForSegue, I pass a reference to the calling view controller. Then I have a method that is called from the modal view.
Here is the Xamarin reference: http://developer.xamarin.com/recipes/ios/general/storyboard/storyboard_a_tableview/
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
{
if (segue.Identifier == "LoginSegue")
{
var destCtrl = segue.DestinationViewController as LoginViewController;
if (destCtrl != null)
{
// pass in a reference to THIS view controller.
destCtrl.SetData(this);
}
}
base.PrepareForSegue(segue, sender);
}
Also in the Calling View Controller
public void LoggedIn (string someFlag)
{
Console.WriteLine("Logged in with : " + someFlag);
DismissViewController(true, null);
}
And the LoginViewController
public void SetData (CallingViewController callingCtrl)
{
calllingViewController = callingCtrl;
}
Then when I am ready to return.
callingViewController.LoggedIn("the flag");

Related

Why Init() method is not called when I navigate to the ViewModel second time?

I develop Win 8.1 application using MvvmCross 3.5.1. The user sequentially goes through the some views and returns to the first view from the last view. Everything works perfect during first iteration of the workflow. But when the user starts the workflow again - Init() methods in viewmodels are not called.
For example, interaction between FirstViewModel and SecondViewModel looks like below.
FirstViewModel:
ShowViewModel<SecondViewModel>(
new
{
code = ItemCode,
descr = ItemDescription
});
SecondViewModel:
public void Init(string code, string descr)
{
...
}
So simple but works only one time :(
What reasons may entail such behavior?
As workaround I tried to load viewmodel "manually":
var d = new Dictionary<string, string>
{
{"code", ItemCode},
{"descr", ItemDescription}
};
var b = new MvxBundle(d);
var r = new MvxViewModelRequest<SecondViewModel>(b, null, null);
var m = Mvx.Resolve<IMvxViewModelLoader>().LoadViewModel(r, null);
It solved the problem with Init() methods calling. But I don't know how to show the viewmodel using the m variable. Anyone knows?
Apologies for my poor english and thanks in advance!
Init() is only being called once, because Windows 8.1 apps cache pages. Hence, the ViewModel for that page is not ever destroyed and hence the Init() method is not called again.
You can make your own BasePage which overrides this behavior by overriding OnNavigatedTo:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.NavigationMode == NavigationMode.New)
ViewModel = null;
base.OnNavigatedTo(e);
}

Call an Activity method from Fragment

I'm using ToDoActiviy.cs for user login, this class got this method:
[Java.Interop.Export()]
public async void LoginUser(View view)
{
if(await authenticate())..
This method is called from .axml file from Button widget android:onClick="LoginUser" I changed this for android:onClick="LoginUserClick" This last method create a dialog fragment for show different logins accounts.
Now from the Dialog Fragment(Is situated on another class) I want to hand the event for the button click on the dialog fragment and call this method from ToDoActivity.cs.
On dialog fragment class I hand the click event like this:
private void ButtonSignInFacebook_Click(object sender, EventArgs args){
//Here code for call to LoginUser method from 'ToDoActivity.cs'
ToDoActiviy.cs act = new ToDoActivity();
act.LoginUser();
}
I need to pass a View but I tried a lot of things and any works..
Someone can help me?
Thanks in advance ;)
I would like to make a slight modification to #guido-gabriel 's answer.
In C# syntax, it will be
((ToDoActivity)Activity).yourPublicMethod();
Getter/Setter Methods in Java are mapped to Getter Setter properties in Xamarin.Android
Finally I fix it ! I had to change the parameters of the method and create it without parameters.. and now Is working. Both solutions are good:
((ToDoActivity)Activity).LoginUserFacebook();
//ToDoActivity act = new ToDoActivity();
//act.LoginUserFacebook();
Adapt and use the snipped below in your fragment
var casted = Activity as MyActivityName;
if (casted != null) {
casted.IWantToCallThisMethodFromMyFragment();
}
You have to call the method from the activity. Have you tried?
((YourActivityClassName)getActivity()).yourPublicMethod();
I.E.
((ToDoActivity)getActivity()).yourPublicMethod();
This is not really a good practice to do. Why?
Doing this couples the Fragment tightly to this particular Activity type, meaning it will not be possible to reuse the Fragment elsewhere in the code.
Instead I suggest you rely on the Activity subscribing to an event or implementing some kind of callback method in order to do the desired action after login.
It could also seem like your Activity might be containing a lot of logic that could be split out into a shared library of some kind. Making it possible to reuse that code on another platform, for instance iOS in the future.
So since your are in charge of newing up the Fragment, I would do something like this instead:
public class LoginFragment : Fragment
{
Action _onLoggedIn;
public static void NewInstance(Action onLoggedIn)
{
var fragment = new LoginFragment();
fragment._onLoggedIn = onLoggedIn;
return fragment;
}
private void Login()
{
// login user
// after loggedin
_onLoggedIn?.Invoke();
}
}
Then in your Activity:
private void LoginUser()
{
// whatever
}
var loginFragment = LoginFragment.NewInstance(LoginUser);
// fragment transaction here...

Caliburn.Micro Go Back called in On Activate not working in WinRT

This question is specific to Windows Phone 8.1 (WinRT); it may also be applicable Windows 8.1. I am using Caliburn.Micro 2.0.1
In my ViewModel's OnActivate I check whether an item is a database, if it isn't, I want to navigate back to the previous page.
The simplist solution will be just to call GoBack in the OnActivate method (this works in Windows Phone 8.0):
INavigationService _navigationService;
protected override void OnActivate()
{
_item = GetItemFromDB();
if(_item == null)
{
_navigationService.GoBack()
}
}
To navigate to the view model I call:
_navigationService.NavigateToViewModel<MyViewModel>(_param);
But it does not work, it ignores the GoBack call and stays on the page which I do not want to view.
When stepping through the code you can see that the GoBack code is called inside the NavigateToViewModel method; I expect this is the reason why it does not work (something to do with a queuing issue maybe?).
I have a very "hacky" solution that involves a timer (that works), but I really despise it since it is prone to threading issues and has the possibility of being called during the NavigateToViewModel call (if it takes long to finish), which will then again not work:
protected override void OnActivate()
{
_item = GetItemFromDB();
if(_item == null)
{
DispatcherTimer navigateBackTimer = new DispatcherTimer();
navigateBackTimer.Interval = TimeSpan.FromMilliseconds(300);
navigateBackTimer.Tick += GoBackAfterNavigation;
navigateBackTimer.Start();
}
}
public void GoBackAfterNavigation(object sender, object e)
{
_navigationService.GoBack();
(sender as DispatcherTimer).Stop();
}
Is there a better way to navigate back? Why doesn't the GoBack work in OnActivate? Is there a way to get it to work in OnActivate?
You can use
Execute.OnUIThreadAsync(() => /* navigationCode */);
instead of a timer to queue the action immediately after the processing of the current stack has finished.

Waiting for a thread to finish in c# / Xamarin

I got a tricky thing here. I am developing an iOS App with Xamarin at the moment. (I think if you don't know about xamarin but c# you might know the answer as well - its an issue with threading basically!) I have a table with several entries, and when the user clicks one of the entries, it is required that i load data from a web service. This data is then displayed in the second table shown another view i push to. The problem is: In the exact moment the user clicks the table entry, the push segue to another view is triggered, the viewDidLoad() there is also triggered, in which i set the tables entries. But now sometimes it occurs that the async networking task from the previous view didn't finish, which leaves the table empty. To sum it up: I need to trigger a Table.Reload() on the View i push to, after the asynchronous networking request has done its work. How do i do that? I am really stuck here. Here are some details of my Code:
First View Controller
public async override void PrepareForSegue (UIStoryboardSegue segue, NSObject sender)
{
//Sends the titles, subtitles, and the Headline to the DetailView
base.PrepareForSegue (segue, sender);
...
Task<String[,]> getJsonDataTask = SearchRequest.searchRequest (text, "Calendar");
arrayContent = await getJsonDataTask;
...
for (int i = 0; i < 50; i++) {
tableItems.Add(arrayContent [1, i]);
tableSubtitles.Add( arrayContent [5,i]);
Dates.Add(tableItems[i]);
Descriptions.Add (tableSubtitles [i]);
}
var calendarDetailController = segue.DestinationViewController as CalendarDetailVController;
if (calendarDetailController != null) {
Console.WriteLine ("preparesegue"+Dates.Count);
calendarDetailController.Dates = Dates;
calendarDetailController.Descriptions = Descriptions;
calendarDetailController.TableTitle = text;
calendarDetailController.id = id;
}
So here i am starting an async Thread (it needs to by ansync. cause its a network request)
And i am transferring the data to the view controller i push to.
In the
Secound View Controller
I just fetch the given data like
public List<String> Dates { get; set; }
public CalendarViewController (IntPtr handle) : base (handle)
{
Dates = new List<String> ();
}
And then i reload the tables data in ViewDidLoad();
public override async void ViewDidAppear (bool animated)
{
base.ViewDidAppear (animated);
//as the resource loading is async. whe need to refresh table here
TableView.ReloadData ();
}
Sometimes, if this ViewDidLoad() gets triggered, AFTER the async task finished, this works. But i need to trigger the Reload when the Data has fully arrived. Do you understand my problem? Thanks for your help.
How about you pass your task instead of the result of the task?
Change:
public List<String> Dates { get; set; }
To:
public Task<String[,]> DatesTask{get;set;}
And you execute the task in the viewDidAppaer of your second viewcontroller:
public override async void ViewDidAppear (bool animated)
{
base.ViewDidAppear (animated);
//as the resource loading is async. whe need to refresh table here
Task.Run (async delegate {
await DatesTask().ContinueWith(() => {
InvokeOnMainThread (delegate {
TableView.ReloadData ();
});
});
});
}
I would not recommend to make a method like ViewDidAppaer wait, instead i would let a task handle the execution of your method, and once it is done, make your tableview refresh. That way you will not freeze your userinterface while executing your task!
Or if you really want to execute your task in your first viewcontroller, you should execute your task BEFORE performing PrepareForSegue .
If I understood your question correctly it seems that you only need to pass the async Task from the view that initiated the call to your second view that needs to await it to finish, nothing fancy here.

Update 2 different classes from a settings flyout?

As per MSDN guidelines we need to put all the app's settings into the SettingsPane and then the app should update all pages when the settings is applied.
In my app I need to have a reset option which brings the app to the default settings. There are 2 pages, Calendar.xaml and HistoryStatistics.xaml that i need to update when the reset button is pressed. All the data of the app is put in a singleton class called CycleManager. I have used a SettingsFlyout control from the Callisto Toolkit.
App.Xaml
Registered the settings in the App.xaml
SettingsPane.GetForCurrentView().CommandsRequested += OnCommandsRequested;
and in OnCommandsRequested function, created the reset handler
var reset = new SettingsCommand("reset", "Reset", (handler) =>
{
var settings = new SettingsFlyout();
settings.Content = new ResetUserControl();
settings.HeaderBrush = new SolidColorBrush(_background);
settings.Background = new SolidColorBrush(_background);
settings.HeaderText = "Reset";
settings.IsOpen = true;
});
args.Request.ApplicationCommands.Add(reset);
CycleManager.cs
In the CycleManager class, there is a m_Reset variable,its setter and getter and an event handler called ResetClicked
public event EventHandler ResetClicked;
public bool Reset
{
get
{
return m_reset;
}
set
{
m_reset = value;
if (ResetClicked != null)
ResetClicked(this, EventArgs.Empty);
}
}
Next is the part where i have associated this handler in my first class calendar.xaml
Calendar.xaml
In the constructor of the class I declare the event handler
CycleManager pCycMan = CycleManager.Instance;
pCycMan.ResetClicked += this.ResetClicked;
followed by the definition of the event handler
private async void ResetClicked(object sender, EventArgs e)
{
CycleManager pCycMan = CycleManager.Instance;
if (pCycMan.Reset == true)
{
try
{
await Windows.Storage.ApplicationData.Current.ClearAsync(Windows.Storage.ApplicationDataLocality.Local);
pCycMan.InitializeValues();
}
catch (Exception)
{
}
}
CreateCalendar();// UI is loaded
}
In the constructor of the HistoryStatistics.xaml I have done the same thing as above
HistoryStatistics.xaml
public HistoryStatistics()
{
CycleManager pCycMan = CycleManager.Instance;
pCycMan.ResetClicked += this.ResetClicked;
}
and defined
private void ResetClicked(object sender, EventArgs e)
{
CycleManager pCycMan = CycleManager.Instance;
if (pCycMan.Reset == true)
{
await Windows.Storage.ApplicationData.Current.ClearAsync(Windows.Storage.ApplicationDataLocality.Local);
pCycMan.InitializeValues();
LoadListView();// loads the UI
DisplayStatistics();//loads the UI for the page
}
}
Now the problem
Is this the right approach?
When Reset is pressed in the first from the second page(HistoryStatistcs), the reset clicked function declared in the first page(Calendar.xaml.cs) is called first and then the one in HistoryStatistics. And both gets executed async! :(
Is this a right behaviour?
This question is quite long. Hope everybody understood the scenario and question.
There is nothing wrong with the behaviour you outlined. Two pages subscribe to an event and event uses multi cast delegate which means they will both get fired.
I think you need a simpler behaviour here. Each xaml page should subscribe to that event on OnNavigatedTo and should unsubscribe in OnNavigatedFrom.
That way only one of the two actually executes the cleanup.
The complexity/confusion is likely coming because of not using the MVVM (model, view, and view model) separation. you may want to read about this. keeping the separation helps. Below are few pointers on this. but not necessarily a full design for your app.
in this example: CycleManager.Instance is kind of serving the model (the data). You may want to rename ResetClicked to SettingChanged and think of the event as notification for clients that one or more settings properties exposed has changed. It should also expose ResetSettings() method that can be called by ResetUserControl.
// model for the settings
class SettingsManager
{
public event EventHandler SettingsChanged;
public async void ResetSettings()
{
await Windows.Storage.ApplicationData.Current.ClearAsync
(Windows.Storage.ApplicationDataLocality.Local);
// initialize all values to default values;
this._intializeValues();
if (this.SettingsChanged != null)
this.SettingsChanged(this, EventArgs.Empty);
}
}
HistoryStatistics and Calendar class should have view model that should listen for SettingsChanged event and update the properties exposed. Each page view (xaml) binds to the properties exposed by the respective view model. This will require some refactoring of current code.
Without that, ResetClick eventhandlers can be changed to SettingChanged event handlers and take required action. They need not call setting mgr to initialize values.
class HistoryStatistics
{
private void SettingsChanged(object sender, EventArgs e)
{
SettingsManager settingsManager = SettingsManager.Instance;
LoadListView();// loads the UI
DisplayStatistics();//loads the UI for the page
}
}
HTH.

Categories

Resources