2019-03-25 12:00:00
Building PepTown with .NET: App Overview
PepTown is our smartphone-based fundraising solution for high-school sports teams. The architecture of PepTown is .NET throughout, including Xamarin.Forms, Reactive Extensions, and ASP.NET Core.
This blog entry is an overview of some of the choices we made when building the client side of PepTown, a Xamarin app for Android and iOS.
Xamarin.Forms, Kinda
We use Xamarin.Forms, but some folks might look at our code and think that we don't.
We don't use XAML. We don't even use the Xamarin.Forms binding mechanism. The only parts that we use are the wrappers around the native controls and the layout system.
Each page (or partial page, or cell) is implemented with a viewmodel. Those viewmodel classes interact with the actual UI controls through an Rx-centric API. This API is quite narrow, containing only the things that a viewmodel would need. For example, the API cares about the text on a label, the items in a list, and the actions that happen on a button tap, but it has no concept of layouts or fonts.
For example, for a Button, the API provided to our viewmodels looks like this:
public Func<TViewModel,IObservable<string>> Text { get; set; } public Func<TViewModel,IObservable<string>> ImagePath { get; set; } public Func<TViewModel,IObservable<Unit>,IDisposable> Click { get; set; } public Func<TViewModel,IObservable<bool>> Enabled { get; set; } public Func<TViewModel,IObservable<bool>> Visible { get; set; }
The button control gets (from the viewmodel) Text, ImagePath, Enabled and Visible. The viewmodel gets (from the button control) notifications of when the button was tapped/clicked.
So all of the business logic for our app resides in assemblies that do not have a dependency on the Xamarin.Forms package. All layout and aesthetic issues are handled at a separate layer
In that layer, for each viewmodel, we have a function that gets an object that contains a reference to all the controls. The job of that function is simply to style those controls, put them into a layout, and return the layout.
For example, here is a snippet from the beginning of the layout function for a football scoreboard:
public View CreateLayout(IXFReadOnlyLiveFootballCompositeViewControls vc) { vc.SchoolName.HorizontalOptions = LayoutOptions.Center; vc.SchoolName.FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)); vc.OpponentName.HorizontalOptions = LayoutOptions.Center; vc.OpponentName.FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)); vc.Versus.HorizontalOptions = LayoutOptions.Center; vc.Versus.FontSize = Device.GetNamedSize(NamedSize.Micro, typeof(Label)); StackLayout schoolsInfoHeader = new StackLayout { Padding = 10, Spacing = 1, Orientation = StackOrientation.Vertical, BackgroundColor = LayoutHelper.DEFAULT_GRAY, Children = { vc.SchoolName, vc.Versus, vc.OpponentName } };
This highlights one of the reasons why we don't use XAML -- we want the creation of widgets to be very separate from the styling and layout of same. In contrast, XAML intermingles the creation and styling and layout of controls all in one place.
Automated UI Testing, Kinda
One of the classic benefits of a viewmodel is that it is testable. Since the viewmodel doesn't have any dependencies on platform or UI specific stuff, we can write tests that create the viewmodel and pretend to be the UI.
One approach we use here is that we implemented a fake Xamarin.Forms library that does not actually create UI controls, but rather, exposes an API we can use to write scripts. Those scripts are simply things that are pretending to be the user.
So we have a scripting version of our "app" which is a console app running under .NET Core. Instead of referencing the real Xamarin.Forms package, it references our fake Xamarin.Forms scripting library.
For example, a typical test script starts with our fake user choosing a favorite school:
contentFrame .FindButton(nameof(ISFWelcomeViewControls.SetFavoriteSchool)) .Tap() ; await contentFrame.WaitForView(typeof(SetFavoriteSchoolView)); contentFrame .FindButton(nameof(ISFSetFavoriteSchoolViewControls.State)) .Tap(); ; await contentFrame.WaitForView(typeof(ChooseStateView)); await contentFrame .FindListView(nameof(ISFChooseStateViewControls.StateItems)) .Items .Where(a => a?.Count > 0) .Take(1) ; contentFrame .FindListView(nameof(ISFChooseStateViewControls.StateItems)) .TapItem<StateCellViewModel>(st => st.Params.Id == "IL") ; await contentFrame.WaitForView(typeof(SetFavoriteSchoolView));
This script, pretending to be a user, taps on a button to brings up the SetFavoriteSchool viewmodel, then it taps on the button to choose a state, and waits for the list of states to get its items, and then taps on the state for Illinois.
A Domain Specific Language, Kinda
It would be basically accurate to say that PepTown is written in C#.
It might be even more accurate to say that the viewmodels are written not in C#, but rather, in a domain specific language (DSL) that is compiled to C#. I'm stretching the definition of a "language" a little bit, because our language doesn't [currently] have a textual representation.
But I still think of it as a DSL. The first stage of a typical compiler takes a textual representation and converts it to a data structure. Well, our DSL has that data structure, and we process it to generate the C# class for the viewmodel.
For example, here is a snippet from the definition of the viewmodel which shows color and mascot information for a school:
.SetButtonAction("MenuEditMascot", new ViewModelInvocation("UpdateSchoolMascot") .AddArgument_PassThru("SchoolId", "Id") )
When someone wants to edit the school mascot info, we bring up the UpdateSchoolMascot viewmodel with the SchoolId as a parameter.
BTW, not only does our DSL not have a textual representation, it also lacks a name. We informally refer to it as "AutoGen".
AutoGen provides us a higher-level abstraction, allowing us to avoid manual repetition of common stuff. The viewmodel mentioned above is defined in 44 lines, but AutoGen converts that into a viewmodel class of over 1,000 lines of C#.
AutoGen also has a somewhat rudimentary type system. For example, we have a SchoolTeams viewmodel that shows a list of teams. The "item tapped" action for that list control will launch the TeamSchedule viewmodel to show the events for that team, so it needs to pass the Team Id as a parameter. For every viewmodel invocation, AutoGen will verify that the parameters are provided and of the proper types. If you try to pass a CoachId into a parameter that requires a TeamId, AutoGen will complain. This allows us to catch errors much earlier.
AutoGen isn't quite expressive enough to describe everything PepTown does, so in a few cases we augment the generated viewmodel class manually. Those handwritten methods amount to less than one percent of all the viewmodel code.