Structuring a MonoTouch.Dialog application - uinavigationcontroller

From the examples at Xamarin.com you can build basic M.T. Dialog apps, but how do you build a real life application?
Do you:
1) Create a single DialogViewController and tree every view/RootElement from there or,
2) Create a DialogViewController for every view and use the UINavigationController and push it on as needed?
Depending on your answer, the better response is how? I've built the example task app, so I understand adding elements to a table, click it to go to the 'next' view for editing, but how to click for non-editing? How to click a button, go next view if answer is number 1?
Revised:
There is probably no one right answer, but what I've come up with seems to work for us. Number 2 from above is what was chosen, below is an example of the code as it currently exists. What we did was create a navigation controller in AppDelegate and give access to it throughout the whole application like this:
public partial class AppDelegate : UIApplicationDelegate
{
public UIWindow window { get; private set; }
//< There's a Window property/field which we chose not to bother with
public static AppDelegate Current { get; private set; }
public UINavigationController NavController { get; private set; }
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
Current = this;
window = new UIWindow (UIScreen.MainScreen.Bounds);
NavController = new UINavigationController();
// See About Controller below
DialogViewController about = new AboutController();
NavController.PushViewController(about, true);
window.RootViewController = NavController;
window.MakeKeyAndVisible ();
return true;
}
}
Then every Dialog has a structure like this:
public class AboutController : DialogViewController
{
public delegate void D(AboutController dvc);
public event D ViewLoaded = delegate { };
static About about;
public AboutController()
: base(about = new About())
{
Autorotate = true;
about.SetDialogViewController(this);
}
public override void LoadView()
{
base.LoadView();
ViewLoaded(this);
}
}
public class About : RootElement
{
static AboutModel about = AboutVM.About;
public About()
: base(about.Title)
{
string[] message = about.Text.Split(...);
Add(new Section(){
new AboutMessage(message[0]),
new About_Image(about),
new AboutMessage(message[1]),
});
}
internal void SetDialogViewController(AboutController dvc)
{
var next = new UIBarButtonItem(UIBarButtonSystemItem.Play);
dvc.NavigationItem.RightBarButtonItem = next;
dvc.ViewLoaded += new AboutController.D(dvc_ViewLoaded);
next.Clicked += new System.EventHandler(next_Clicked);
}
void next_Clicked(object sender, System.EventArgs e)
{
// Load next controller
AppDelegate.Current.NavController.PushViewController(new IssuesController(), true);
}
void dvc_ViewLoaded(AboutController dvc)
{
// Swipe location: https://gist.github.com/2884348
dvc.View.Swipe(UISwipeGestureRecognizerDirection.Left).Event +=
delegate { next_Clicked(null, null); };
}
}
Create a sub-class of elements as needed:
public class About_Image : Element, IElementSizing
{
static NSString skey = new NSString("About_Image");
AboutModel about;
UIImage image;
public About_Image(AboutModel about)
: base(string.Empty)
{
this.about = about;
FileInfo imageFile = App.LibraryFile(about.Image ?? "filler.png");
if (imageFile.Exists)
{
float size = 240;
image = UIImage.FromFile(imageFile.FullName);
var resizer = new ImageResizer(image);
resizer.Resize(size, size);
image = resizer.ModifiedImage;
}
}
public override UITableViewCell GetCell(UITableView tv)
{
var cell = tv.DequeueReusableCell(skey);
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Default, skey)
{
SelectionStyle = UITableViewCellSelectionStyle.None,
Accessory = UITableViewCellAccessory.None,
};
}
if (null != image)
{
cell.ImageView.ContentMode = UIViewContentMode.Center;
cell.ImageView.Image = image;
}
return cell;
}
public float GetHeight(UITableView tableView, NSIndexPath indexPath)
{
float height = 100;
if (null != image)
height = image.Size.Height;
return height;
}
public override void Selected(DialogViewController dvc, UITableView tableView, NSIndexPath indexPath)
{
//base.Selected(dvc, tableView, path);
tableView.DeselectRow(indexPath, true);
}
}
#miquel
The current idea of a workflow is an app that starts with a jpg of the Default.png that fades into the first view, with a flow control button(s) that would move to the main app. This view, which I had working previous to M.T.D. (MonoTouch.Dialog), which is a table of text rows with an image. When each row is clicked, it moves to another view that has the row/text in more detail.
The app also supports in-app-purchasing, so if the client wishes to purchase more of the product, then switch to another view to transact the purchase(s). This part was the main reason for switching to M.T.D., as I thought M.T.D. would be perfect for it.
Lastly there would be a settings view to re-enable purchases, etc.
PS How does one know when the app is un-minimized? We would like to show the fade in image again.

I have been asking myself the same questions. I've used the Funq Dependency Injection framework and I create a new DialogViewController for each view. It's effectively the same approach I've used previously developing ASP.NET MVC applications and means I can keep the controller logic nicely separated. I subclass DialogViewController for each view which allows me to pass in to the controller any application data required for that particular controller. I'm not sure if this is the recommended approach but so far it's working for me.
I too have looked at the TweetStation application and I find it a useful reference but the associated documentation specifically says that it isn't trying to be an example of how to structure a MonoTouch application.

I use option 2 that you stated as well, it works pretty nicely as you're able to edit the toolbar options on a per-root-view basis and such.

Option 2 is more feasible, as it also gives you more control on each DialogViewController. It can also helps if you want to conditionally load the view.

Related

Notify Activity of changes in viewModel

I try to exit 'lock task mode' in Xamarin Android app. Here is what I am trying to achieve:
User taps on label (view in Xamarin.Forms) -> it cause change in ViewModel's boolean property to true
MainActivity (Xamarin.Android) observe that property has changed to true -> it makes application exit 'lock task mode'
My viewModel is placed in Xamarin.Forms 'App.xaml' class so it is accessible in Forms and Android part.
How Can I notify my Activity that property has changed so it can exit locked mode? I know this is propably very poor workaround, I would love to hear any advices and tips to make it more professional.
Thank you in advance!
EDIT
So the point is that I have got ViewModel with boolean property exitLockMode which indicates if app should be in lock mode or not:
public class AdminViewModel : BaseViewModel
{
//Number of taps to touch at main banner in 'MainPage' to open Admin Window
private int _tapsRequiredToAdmin = 5;
//Number of tolerance in miliseconds between next taps
private int _toleranceInMs = 1000;
private bool _exitLockMode = false;
public int ToleranceInMs { get => _toleranceInMs; }
public int TapsRequiredToAdmin { get => _tapsRequiredToAdmin; }
public bool ExitLockMode
{
get => _exitLockMode;
set => _exitLockMode=value;
}
}
AdminViewModel is created in 'App.xaml' class:
public partial class App : Application
{
private static AdminViewModel _adminViewModel;
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new MainPage());
}
public static AdminViewModel AdminViewModel
{
get
{
if(_adminViewModel == null )
_adminViewModel = new AdminViewModel();
return _adminViewModel;
}
}
protected override void OnStart() { }
protected override void OnSleep() { }
protected override void OnResume() { }
}
In my main view (Xamarin.Forms) I have got label where admin want to tap few times in order to exit lock mode:
private DateTime? LastTap = null;
private byte NumberOfTaps = 0;
AdminViewModel adminViewModel = App.AdminViewModel;
**********************************************
//This is method binded to Label in <TapGestureRecognizer Tapped="OnLabelTapped">
private async void OnLabelTapped(object sender, EventArgs e)
{
if (LastTap == null || (DateTime.Now - LastTap.Value).TotalMilliseconds < adminViewModel.ToleranceInMs)
{
if (NumberOfTaps == (adminViewModel.TapsRequiredToAdmin - 1))
{
NumberOfTaps = 0;
LastTap = null;
adminViewModel.ExitLockMode = true;
return;
}
else
{
NumberOfTaps++;
LastTap = DateTime.Now;
}
}
else
{
NumberOfTaps = 1;
LastTap = DateTime.Now;
}
}
Now I want to achieve that when I turn 'ExitLockMode' bool to true, it notify my 'MainActivity' (Xamarin.Android) to fire 'StopLockTask()' method. I know that in native Android it could be handled by observing bool property, but I don't know how to do it here.
I am newbie so it could be very messy, every help appreciated.
As Jason said, you can use messagecenter.The Xamarin.Forms MessagingCenter class implements the publish-subscribe pattern, allowing message-based communication between components that are inconvenient to link by object and type references.
This mechanism allows publishers and subscribers to communicate without having a reference to each other, helping to reduce dependencies between them.
You can follow this document and the sample in it https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/messaging-center

Xamarin.Forms Update Label Text From a Service

I have a Label on MainPage.xaml. I can edit Label Text from MainPage.xaml.cs.
There is a foregroung service running as well. There is function in this service to check a value from SQLite DB for every 10 secs. When value changes, Label text should be updated. I tried binding but it is a bit confusing. I manage updating by using like this: (foreground service timer changes App.SomeValue)
protected override void OnAppearing()
{
lblSyncID.Text = App.SomeValue;
}
But I need to see changes without OnAppearing or any other navigation change.
EDIT:
With #Jason's suggestion I used Messaging Center (and also binding) and it works now:
MainPage.xaml:
<Label Text="{Binding AppWaitingRecordValue}" ...
MainPage.xaml.cs:
public partial class MainPage : ContentPage
{
private string appWaitingRecordValue;
public string AppWaitingRecordValue
{
get { return appWaitingRecordValue; }
set
{
appWaitingRecordValue = value;
OnPropertyChanged(nameof(AppWaitingRecordValue));
}
}
public MainPage()
{
InitializeComponent();
BindingContext = this;
AppWaitingRecordValue = "0";
MessagingCenter.Subscribe<App>((App)Application.Current, "AppRecord", (sender) =>
{
AppWaitingRecordValue = App.recordWaiting.ToString();
});
}
.
.
TimestampService.cs (from Project.Android):
// get i from DB
App.recordWaiting = i;
Xamarin.Forms.MessagingCenter.Send<App>((App)Xamarin.Forms.Application.Current, "AppRecord");

Navigation Drawer: how make fragments persistent (keep alive) while switching (not rotating)

With Fragment:setRetainInstance(true); the fragment is not re-instantiated on a phones orientation change.
And of course i want my fragments to be kept alive while switching from one fragment to another.
But the Android Studio 4 provides a wizard-template with only
DrawerLayout drawer = findViewById(R.id.drawer_layout);
NavigationView navigationView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder(
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow)
.setDrawerLayout(drawer)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
NavigationUI.setupWithNavController(navigationView, navController);
From hours of debugging and searching the net if think it would need to inherent from the class FragmentNavigator so i can overwrite FragmentNavigator:naviagte where a new fragment gets created via final Fragment frag = instantiateFragment(.. and then is added with ft.replace(mContainerId, frag);
So i could find my old fragment and use ftNew.show and ftOld.hide instead.
Of course this is a stupid idea, because this navigate method is full of other internal stuff.
And i have no idea where that FrameNavigator is created.
I can retrieve it in the MainActivity:OnCreate with
NavigatorProvider navProvider = navController.getNavigatorProvider ();
Navigator<NavDestination> navigator = navProvider.getNavigator("fragment");
But at that time i could only replace it with my derived version. And there is no replaceNavigtor method but only a addNavigator method, which is called where ?
And anyways this all will be far to complicated and therefore error prone.
Why is there no simple option to keep my fragments alive :-(
In older Wizard-Templates there was the possibility of
#Override
public void onNavigationDrawerItemSelected(int position) {
Fragment fragment;
switch (position) {
case 1:
fragment = fragment1;
break;
case 2:
fragment = fragment2;
break;
case 3:
fragment = fragment3;
break;
}
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if(mCurrentFragment == null) {
ft.add(R.id.container, fragment).commit();
mCurrentFragment = fragment;
} else if(fragment.isAdded()) {
ft.hide(mCurrentFragment).show(fragment).commit();
} else {
ft.hide(mCurrentFragment).add(R.id.container, fragment).commit();
}
mCurrentFragment = fragment;
}
but i have no idea how to do this with the Android 4.0 template where my MainActivity is only derived as:
public class MainActivity extends AppCompatActivity {
private AppBarConfiguration mAppBarConfiguration;
Ideas welcome :'(
Hi there & sorry for my late answer! I had a similar problem with navigation drawers and navigation component. I tried around a little and found a working solution, which might be helpful for others too.
The key is the usage of a custom FragmentFactory in the FragmentManager of the MainActivity. See the code for this below:
public class StaticFragmentFactory extends FragmentFactory {
private myNavHostFragment1 tripNavHostFragment;
private myNavHostFragment2 settingsNavHostFragment;
#NonNull
#Override
public Fragment instantiate(#NonNull ClassLoader classLoader, #NonNull String className) {
if (MyNavHostFragment1.class.getName().equals(className)) {
if (this.myNavHostFragment1 == null) {
this.myNavHostFragment1 = new MyNavHostFragment1();
}
return this.myNavHostFragment1 ;
} else if (MyNavHostFragment2.class.getName().equals(className)) {
if (this.myNavHostFragment2 == null) {
this.myNavHostFragment2 = new MyNavHostFragment2();
}
return this.myNavHostFragment2;
}
return super.instantiate(classLoader, className);
}
}
The FragmentFactory survives the navigation between different fragments using the NavigationComponent of AndroidX. To keep the fragments alive, the FragmentFactory stores an instance of the fragments which should survive and returns this instance if this is not null. You can find a similar pattern when using a singleton pattern in classes.
You have to register the FragmentFactory in the corresponding activity by calling
this.getSupportFragmentManager().setFragmentFactory(new StaticFragmentFactory())
Please note also that I'm using nesten fragments here, so one toplevel fragment (called NavHostFragmen here) contains multiple child fragments. All fragments are using the same FragmentFactory of their parent fragments. The custom FragmentFactory above returns the result of the super class method, when the fragment to be instantiated is not known to keep alive.

Master Detail Page is not appearing after Content page

I am working in xamarin.forms. I am creaing an android application. In my application I have to use menu. So I took Master detail page to show the menus. And its working fine.
But my problem is before showing the Master detail page, I have to open a content page which doesn't contain the menus. So I took a content page and set it. But when I am running the application after content page, Master detail page is not appearing. The code is running successfully but the Master page is not appearing.
Can any one tell me how I can show the Master detail page after showing simple content page?
The answer will depend on if you want to maintain the navigation stack or not. If you want to add the new page to the current Navigation Stack then you need to so something like this in the Content Page:
((NavigationPage)Parent).PushAsync(newPage);
If you want to make the new page the root of the Navigation Stack then you need to do something like this:
((App) Parent).MainPage = newPage;
If this doesn't work, post your code.
Giving you a trick ! suppose you have a login page ,after authentication you will go to RootPage which is a masterDetailPage .
Take a hint from from below code
namespace LoginNavigation
{
public class App : Application, IloginInterface
{
public static App current;
public static bool IsUserLoggedIn { get; set; }
public static double ScreenWidth;
public static double ScreenHeight;
public App () {
current = this;
MainPage = new LoginPageWithStack ();
}
public void Logout() {
MainPage = new LoginPageWithStack ();
}
public void ShowMainPage() {
MainPage = new RootPage ();
}
}
}
rootPage:
namespace LoginNavigation
{
public class RootPage:MasterDetailPage
{
MenuPage menuPage;
public RootPage () {
ToolbarItems.Add(new ToolbarItem("Filter", "ring.png", async () => {
var page = new ContentPage();
var result = await page.DisplayAlert("Title", "Message", "Accept", "Cancel");
Debug.WriteLine("success: {0}", result);
}));
menuPage = new MenuPage ();
menuPage.Menu.ItemSelected += (sender, e) => NavigateTo (e.SelectedItem as MenuItemForMaster);
//Master = new MasterMenu();
Master = menuPage;
Detail = new NavigationPage (new TimeSheet()){
BarBackgroundColor = Color.FromHex("008dce"),BackgroundColor = Color.FromHex("008dce")
};
}
void NavigateTo (MenuItemForMaster menu) {
if (menu == null)
return;
Page displayPage = (Page)Activator.CreateInstance (menu.TargetType);
//Detail = displayPage;
Detail = new NavigationPage (displayPage) { BarBackgroundColor = Color.FromHex("008dce"),BackgroundColor = Color.FromHex("008dce")};
menuPage.Menu.SelectedItem = null;
IsPresented = false;
}
}
}
So the trick is ,get the current instance of App class and manipulate Mainpage property of it .

AspectJ capture button clicked

I want to know whether how to capture the button clicked with AspectJ and get its parameter (eg. button name). I think for having more generalized capturing with AspectJ, it shoudl be used MouseListener so it can capture other UI elements in general!
Example:
In a GUI example I have defined 2 buttons that take some actions
public JButton btn1 = new JButton("Test1");
public JButton btn2 = new JButton("Test2");
btn1.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
//take some actions
}
}
btn2.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
//take some actions
}
}
How to capture these buttons with AspectJ, and get their parameters (eg. name)?
It is possible. I have provided two examples. The first that prints out for every JButton that has an ActionListener. The other example only prints out if a specific buttons is clicked.
Prints the text for every JButton clicked with an ActionListener:
#Pointcut("execution(* *.actionPerformed(*)) && args(actionEvent)")
public void buttonPointcut(ActionEvent actionEvent) {}
#Before("buttonPointcut(actionEvent)")
public void beforeButtonPointcut(ActionEvent actionEvent) {
if (actionEvent.getSource() instanceof JButton) {
JButton clickedButton = (JButton) actionEvent.getSource();
System.out.println("Button name: " + clickedButton.getText());
}
}
Prints the text for a specific JButton:
public static JButton j1;
#Pointcut("execution(* *.actionPerformed(*)) && args(actionEvent) && if()")
public static boolean button1Pointcut(ActionEvent actionEvent) {
return (actionEvent.getSource() == j1);
}
#Before("button1Pointcut(actionEvent)")
public void beforeButton1Pointcut(ActionEvent actionEvent) {
// logic before the actionPerformed() method is executed for the j1 button..
}
UPDATED:
You can do this in many different ways. For example add your buttons to the aspect directly. But I prefere to use a enum object between (ButtonManager in this case), so the code does not know about the aspect. And since the ButtonManager is an enum object, it is easy for the aspect to retrieve values from it.
I just tested it with a Swing button class from Oracle and it works. In the Swing class:
b1 = new JButton("Disable middle button", leftButtonIcon);
ButtonManager.addJButton(b1);
AspectJ is extremely powerful when it comes to manipulating classes, but it can not weave advises into specific objects since objects is not created at the time of weaving. So you can only work with objects at runtime and that is why I have added the addJButton(..) method above. That enables the aspect to check the advised button against a list of registered buttons.
The ButtonManager class:
public enum ButtonManager {
;
private static Collection<JButton> buttonList = new LinkedList<JButton>();
public static void addJButton(JButton jButton) {
buttonList.add(jButton);
}
public static Collection<JButton> getButtonList() {
return buttonList;
}
}
Modified pointcut and advice to only print the name of the buttons registered in the ButtonManager:
#Pointcut("execution(* *.actionPerformed(*)) && args(actionEvent) && if()")
public static boolean buttonListPointcut(ActionEvent actionEvent) {
Collection<JButton> buttonList = ButtonManager.getButtonList();
JButton registeredButton = null;
for (JButton jButton : buttonList) {
if (actionEvent.getSource() == jButton) {
registeredButton = jButton;
}
}
return registeredButton != null;
}
#Before("buttonListPointcut(actionEvent)")
public void beforeButtonListPointcut(ActionEvent actionEvent) {
JButton clickedButton = (JButton) actionEvent.getSource();
System.out.println("Registered button name: " + clickedButton.getText());
}
UPDATED 2
Okay, I believe I understand what you want. You want to listen to mouse events. That is possible. The downside is that you have to register all your GUI components that you want to listen for clicks with a mouse listener. It is not enough to register the JPanel of the JFrame with a MouseListener. So if you only have registered an ActionListener for your buttons, you also have to add a mouse listener.
I have created a quick solution that works for me. It only shows that it works. I have not tried to make the solution generic with many different GUI objects. But that should be quite easy to refactor in when you have got the basics to work.
In the Swing class:
private class MouseListener extends MouseInputAdapter {
public void mouseClicked(MouseEvent e) {}
}
In the init method of the Swing class:
MouseListener myListener = new MouseListener();
btn1.addMouseListener(myListener);
btn2.addMouseListener(myListener);
In the Aspect class:
#Pointcut("execution(* *.mouseClicked(*)) && args(mouseEvent)")
public void mouseEventPointcut(MouseEvent mouseEvent) {}
#Before("mouseEventPointcut(mouseEvent)")
public void beforeMouseEventPointcut(MouseEvent mouseEvent) {
if (mouseEvent.getSource() instanceof JButton) {
JButton clickedButton = (JButton) mouseEvent.getSource();
System.out.println("aspectJ --> mouseClicked: " + clickedButton.getText());
}
}
This results in the following output in the console:
aspectJ --> mouseClicked: Test1
I hope it helps!

Resources