Ambie is a UWP app that runs on desktops, laptops, tablets, and Xbox consoles with one codebase. It has been this way since version 1.0 and that hasn’t changed in 4.0. Reminder that you can find Ambie’s entire codebase on GitHub: https://github.com/jenius-apps/ambie.
Truthfully, no major structural changes occurred in the building of Ambie 4.0. In fact, it still uses the same underlying system as 3.0, even though the UI layer was nearly rewritten from scratch. This modularity is precisely why I wanted to write about Ambie’s architecture.
MVVMS
Ambie implements the MVVMS design pattern. This method of compartmentalizing groups of code allows the app to be modular. I can change one component without affecting the others as long as I don’t introduce a breaking change.
The Views layer, or the UI layer, represents the app’s various XAML pages and UserControls. These are classes that live inside the Universal Windows project and depend completely on the platform’s UI elements. In Ambie 4.0, the layer that had the majority of changes was the Views layer. The other layers were practically untouched.
The ViewModel layer represents all of Ambie’s ViewModel classes, such as the ShellPageViewModel or the OnlineSoundViewModel. These classes are the orchestrators or the bridges that take input from the user via the Views layer and passes it off to the Services layer. In the MVVMS design pattern, ViewModels do not hold business logic. They only hold orchestration logic. For instance, let’s say a user provided input in the UI layer. The ViewModel received this and passes it off to a service class. The service class returns an output, and the ViewModel will then update a boolean value. The boolean value is observed by the UI and a visual change occurs. You can see here that the ViewModel orchestrated that flow of logic, but it didn’t actually perform the core piece of logic. This keeps ViewModels minimal and only responsible with passing inputs and outputs.
The Services layer represents all the other classes that perform business logic. Sending a POST message to an HTTP endpoint is the job of a client class. This class would be part of the broad “Services” layer. It’s business logic that can be performed regardless of what UI is being displayed, and thus this added to the Services layer so it remains completely decoupled from the View and the ViewModel.
Lastly we have the Models layer which represents the data types that Ambie works with. Sound.cs is a model, as well as Video.cs. These classes contain properties representing the object. There’s no business logic and certainly nothing UI centric. A model should have no dependencies.
The way that Ambie is layered allowed me to maximize decoupling and modularity. I can modify entire swaths of Ambie’s code with high confidence that other parts will work just fine (again, as long as I don’t break API interfaces).
Dependency Injection
In concert with the MVVMS design pattern, Ambie strictly adheres to constructor injection. For example, Each XAML page is backed by a ViewModel. The ViewModel is registered in App.Configuration.xaml.cs, and each ViewModel describes its dependencies in the constructor. With this approach, you can clearly see that ViewModels have a dependency on many services, matching the flow of dependency in the MVVMS diagram.
This approach allows me to modify implementations of various services without modifying the ViewModel. What exactly does that mean? Since ViewModels have dependencies on interfaces, I can register a new or a different implementation of the interface. The new class can have completely different logic, but as long as it adheres to the interface, then there are no changes required in the ViewModel. Perfectly modular!
For more information on implementing dependency injection in your UWP app, read this article: https://medium.com/@kidjenius/dependency-injection-in-uwp-apps-82e6eebf9e23.