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!