Creating an overloadable help interface

Jesse Knott 686 Reputation points
2021-09-25T17:44:24.44+00:00

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!

Xamarin
Xamarin
A Microsoft open-source app platform for building Android and iOS apps with .NET and C#.
5,291 questions
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Leon Lu (Shanghai Wicresoft Co,.Ltd.) 68,016 Reputation points Microsoft Vendor
    2021-09-27T03:38:00.077+00:00

    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://learn.microsoft.com/en-us/xamarin/xamarin-forms/xaml/resource-dictionaries