question

JesseKnott-2239 avatar image
0 Votes"
JesseKnott-2239 asked LeonLu-MSFT edited

Creating an overloadable help interface

I am working on making a system for advanced help pages in my app.
I wanted to make a basic template page that could just have strings passed to it, to display the desired help text. However I also wanted to be able to show graphics and so on.
To accomplish this, I simply exposed a StackLayout that I can populate using any View object I want to make.

I am curious, I know this is more of a style/preference question, so I am just looking for opinions here.

This is the ContentView that I'm using for the help page (More or less a generic onboarding page)

  I don't know if you need to see my xaml, but I can't seem to post xaml in this post. it gives me an error saying unauthorized action when I try,

the page Codebehind is simply

 public partial class OnboardingPage : ContentPage
    {
        /// <summary>
        /// Defines the viewModel
        /// </summary>
        private OnboardingViewModel viewModel;
    
        /// <summary>
        /// Initializes a new instance of the <see cref="OnboardingPage" /> class.
        /// </summary>
        /// <param name="vm"> The vm <see cref="OnboardingViewModel" /> </param>
        public OnboardingPage(OnboardingViewModel vm)
        {
            InitializeComponent();
            viewModel = vm ?? new OnboardingViewModel();
            BindingContext = viewModel?.Pages?[0];
            viewModel.Pg = 0;
            if (viewModel?.Pages?.Count <= 1)
            {
                // Need to populate some default data
            }
    
            btnPrev.Clicked += BtnPrev_Clicked;
            btnCenter.Clicked += BtnCenter_Clicked;
            btnNext.Clicked += BtnNext_Clicked;
            // Task.Delay(500);
        }
    
        /// <summary>
        /// The BtnCenter_Clicked
        /// </summary>
        /// <param name="sender"> The sender <see cref="object" /> </param>
        /// <param name="e">      The e <see cref="EventArgs" /> </param>
        private void BtnCenter_Clicked(object sender, EventArgs e)
        {
            try
            {
                //Check if there is an action we were supposed to execute and do so
                if (viewModel.Pages[viewModel.Pg]?.CenterLabelAction != null)
                {
                    viewModel.Pages[viewModel.Pg]?.CenterLabelAction.Execute(null);
                }
            }
            catch (Exception ex)
            {
                DebugTools.LogException(ex);
            }
        }
    
        /// <summary>
        /// The BtnNext_Clicked
        /// </summary>
        /// <param name="sender"> The sender <see cref="object" /> </param>
        /// <param name="e">      The e <see cref="EventArgs" /> </param>
        private void BtnNext_Clicked(object sender, EventArgs e)
        {
            try
            {
                //pre-increment the page number and make sure the page count is still equal or higher.
                if ((viewModel.Pages?.Count) >= ++viewModel.Pg)
                {
                    BindingContext = viewModel.Pages[viewModel.Pg];
    
                    // Check the main page content
                    if (viewModel.Pages[viewModel.Pg]._MainStack != null)
                    {
                        MainStack.Children.Clear();
                        MainStack.Children.Add(viewModel.Pages[viewModel.Pg]._MainStack);
                    }
    
                    // Check the content area stack
                    if (viewModel.Pages[viewModel.Pg]._ContentStack != null)
                    {
                        ContentStack.Children.Clear();
                        ContentStack.Children.Add(viewModel.Pages[viewModel.Pg]._ContentStack);
                    }
    
                    // Check the button area stack
                    if (viewModel.Pages[viewModel.Pg]._ButtonStack != null)
                    {
                        ButtonStack.Children.Clear();
                        ButtonStack.Children.Add(viewModel.Pages[viewModel.Pg]._ButtonStack);
                    }
    
                    //Check if there is an action we were supposed to execute and do so
                    if (viewModel.Pages[viewModel.Pg]?.NextLabelAction != null)
                    {
                        viewModel.Pages[viewModel.Pg]?.NextLabelAction.Execute(null);
                    }
                }
            }
            catch (Exception ex)
            {
                DebugTools.LogException(ex);
            }
        }
    
        /// <summary>
        /// The BtnPrev_Clicked
        /// </summary>
        /// <param name="sender"> The sender <see cref="object" /> </param>
        /// <param name="e">      The e <see cref="EventArgs" /> </param>
        private void BtnPrev_Clicked(object sender, EventArgs e)
        {
            try
            {
                if ((viewModel.Pg - 1) >= 0)
                {
                    BindingContext = viewModel.Pages[--viewModel.Pg];
    
                    // Check the main page content
                    if (viewModel.Pages[viewModel.Pg]._MainStack != null)
                    {
                        MainStack.Children.Clear();
                        MainStack.Children.Add(viewModel.Pages[viewModel.Pg]._MainStack);
                    }
    
                    // Check the content area stack
                    if (viewModel.Pages[viewModel.Pg]._ContentStack != null)
                    {
                        ContentStack.Children.Clear();
                        ContentStack.Children.Add(viewModel.Pages[viewModel.Pg]._ContentStack);
                    }
    
                    // Check the button area stack
                    if (viewModel.Pages[viewModel.Pg]._ButtonStack != null)
                    {
                        ButtonStack.Children.Clear();
                        ButtonStack.Children.Add(viewModel.Pages[viewModel.Pg]._ButtonStack);
                    }
    
                    //Check if there is an action we were supposed to execute and do so
                    if (viewModel.Pages[viewModel.Pg]?.PreviousLabelAction != null)
                    {
                        viewModel.Pages[viewModel.Pg]?.PreviousLabelAction.Execute(null);
                    }
                }
            }
            catch (Exception ex)
            {
                DebugTools.LogException(ex);
            }
        }
    }

The viewmodel is simply the series of containers for the data.

My other concern is how to go about creating these pages? It's somewhat complex to add the pages in code, but not impossible, see the following example. This is the event handler for when the user taps on the help icon.

 private async void LendUsAHand_Clicked(object sender, EventArgs e)
 {
     OnboardingViewModel ob = new OnboardingViewModel();

     ob.Pages.Add(new ViewModels.OBPage
     {
         ProgressText = StringTools.GetStringResource("HelpPage1Title"),
         Progress = 10,
         _ContentStack = new StackLayout()
         {
             Children =
             {
                 new Label()
                 {
                     Text = StringTools.GetStringResource("HelpPage1Body")
                 }
             }
         },
         NextLabelVis = true
     });

     ob.Pages.Add(new ViewModels.OBPage
     {
         ProgressText = "Done",
         Progress = 100,
         CenterLabelVis = true,
         CenterLabel = "Done",
         CenterLabelAction = new Command(
             execute: () =>
             {
                 this.Navigation.PopAsync();
             })
     });

     if (Navigation.NavigationStack[Navigation.NavigationStack.Count - 1].GetType() != typeof(OnboardingPage))
         await Navigation.PushAsync(new OnboardingPage(ob));
 }

My other thought was to have a series of folders that would have ContentView pages defined in them which would represent each individual step. At which point the above code becomes simply.

 private async void LendUsAHand_Clicked(object sender, EventArgs e)
 {
     .....
     ob.Pages.Add(new ViewModels.OBPage
     {
         ProgressText = StringTools.GetStringResource("HelpPage1Title"),
         Progress = 10,
         _ContentStack = new HelpPage1Body(),
         NextLabelVis = true
     });
     .....
 }

In this layout I use a function wrapper I wrote to get strings from resource files.


  public static string GetStringResource(string name)
  {
      try
      {
          if (string.IsNullOrEmpty(name))
          {
              return "";
          }
 
 
          var ret = string.Empty;
          var temp = new System.Resources.ResourceManager(
                                  "BoomStick.Properties.Resources",
                                  typeof(App).GetTypeInfo().Assembly);
 
          ret = temp.GetString(name, null);
          return ret;
      }
      catch (System.Resources.MissingManifestResourceException mmre)
      {
          Debug.WriteLine($"GetStringResource: Got an exception->{mmre.Message}==={mmre.InnerException}");
      }
      catch (Exception ex)
      {
          DebugTools.LogException(ex);
      }
      return "";
  }

At this time, this function is statically coded to look only at the base Resources file.
I was considering adding a parameter to the function, that would allow the dev to specify a string that would be the local Resources file, like so.


  //(Code simplified for brevity)
  public static string GetStringResource(string name, string szResxFilePath = "BoomStick.Properties.Resources")
  {
          return  new System.Resources.ResourceManager(
                                  szResxFilePath,
                                  typeof(App).GetTypeInfo().Assembly).GetString(name, null);
  }

I was also thinking that I could use System.Reflection for getting the file?
Is there a way I could tell the app to look at the calling function, and see if there is a resources file associated with it, then use it, or default like I did in the other example.?
I ask this, because the way I plan to organize the resources files is to have them linked in the compiler like codebehind files.


    MyPage.xaml
    |____MyPage.xaml.Resources
    |____MyPage.xaml.cs


This would make keeping track of the files and strings easier, since they would be local to their use. I could also add images such as screen shots and so on to the resources file to make examples of steps easier.

I know this is a lot of questions, and opinions, but any feedback would be greatly appreciated!

Cheers!

dotnet-xamarinforms
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

1 Answer

LeonLu-MSFT avatar image
0 Votes"
LeonLu-MSFT answered LeonLu-MSFT edited

Hello,​

Welcome to our Microsoft Q&A platform!

is OnboardingPage a temple page? All of the pages will be add it and show it? If so, please do not use this way to display page, if temple page always in the foreground, and is not released, it will caused the memory leak(for example, your every page have some properties, when current page is replaced by other page in temple page, but previous page's properties do not be released).

About Resources and style, why not to use [resource-dictionaries][2]. For example, if you have a Application.Resources in the app.xaml.cs

<Application.Resources>
        <Style x:Key="labelBlueStyle" TargetType="Label">
            <Setter Property="TextColor" Value="Blue" />
            <Setter Property="Text" Value="this is a text" />
            
        </Style>
    </Application.Resources>


Then in the page's xaml, you can use it directly.

<Label 
                   Style="{StaticResource labelBlueStyle}" />

If you want to use this Resources in the page.xml, just move Style to the ResourceDictionary of ContentPage.Resources.

Best Regards,

Leon Lu



If the response is helpful, please click "Accept Answer" and upvote it.

Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.


 [2]: https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/resource-dictionaries

· 3
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

'OnboardingPage' is a template, but only only gets created when called. The execution would be like
1) user loads page with normal content,
2) user taps on help icon.
3) tap handler creates an observablecollection<onboardingviewmodel> and pushes template page to the stack using Navigation.PushAsync and passes the collection of the viewmodels.
4)navigation (Next, Previous) buttons change the page content. The "Done" button pops the page off the stack.

I am making assumptions here, but I was figuring that the, tap handler would block until the OnboardingPage returns, at which point it would handle the garbage collection for the viewmodel.
I could always ensure the garbage collector is called in the OnboardingPage destructor if that would be better?

For the time being I've actually made a workaround that is working for the resource handling.

My resx files are all stored in the Properties folder, and a line of code appends the prefix to the path within the GetStringResource code. Now the file tree looks like this

 Properties
     |__ Resources.resx   //Base resources file contains common strings to all pages
     |__ MyPage.resx  //Contains the page specific strings.

0 Votes 0 ·

  public static string GetStringResource(string name, string szResourceFile = "Resources")
         {
             try
             {
                 if (string.IsNullOrEmpty(name))
                 {
                     return "";
                 }
    
                 var ret = string.Empty;
                 var temp = new System.Resources.ResourceManager(
                                         "BoomStick.Properties." + szResourceFile,
                                         typeof(App).GetTypeInfo().Assembly);
    
                 ret = temp.GetString(name, null);
                 return ret;
             }
             catch (System.Resources.MissingManifestResourceException mmre)
             {
                 Debug.WriteLine($"GetStringResource: Got an exception->{mmre.Message}==={mmre.InnerException}");
             }
             catch (Exception ex)
             {
                 DebugTools.LogException(ex);
             }
             return "";
         }

I can then call GetStringResource("MyString"); This would default to the common strings file. But I could also call GetStringResource("MySpecificString", "MyPage");

Let me know if the idea about OnboardingPage is accurate or not, I could always make them static pages that have static content. I was hoping to be able to make the pages more dynamic for the sake of not having to manage making tons of pages to manage individually.

Thanks!

0 Votes 0 ·

Using System.Resources.ResourceManager( "BoomStick.Properties." + szResourceFile,typeof(App).GetTypeInfo().Assembly); to read resx file that is ok, but I do not test this perfomace when used in large quantities.

0 Votes 0 ·