The Twenty Percent

Goal

Goal

Create an app which lets the user log data series on different topics. The topics can have a title, a description and an image. The data series are based on user-defined tags.

repo

Previous posts

Fixing the database

There’s somthing wrong with the way I’m storing records in the database. Somehow data is getting lost / not persisted properly.

This is the exception that’s being thrown every time I’m updating an entity (e.g. Topic):

The instance of entity type ‘Topic’ cannot be tracked because another instance with the same key value for {‘Id’} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using ‘DbContextOptionsBuilder.EnableSensitiveDataLogging’ to see the conflicting key values.

This article goes through the basics of EF Core with Xamarin. It’s made me aware of migrations, but I’ll look into that later.

Tests

Wrote a bunch of tests to avoid having to boot the Android Emulator for every little change. It looks like I’ve also fixed the Entity Update bug.

C# 9

Upgraded to C# 9, and made the database entities records, which could make the ToViewModel/ToModel functions refactorable!

Eager Loading

There are various ways to handle the loading of related entities. I’m using eager Loading in some places (load tags when loading data series). It looks like there are many considerations to take in that article, so if I want to do more advanced stuff with this, I should read up on the caveats.

Disposing Context and Dependency Injection

This error message pops up when I try to add a DataSeries:

System.ObjectDisposedException: ‘Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling ‘Dispose’ on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: ‘PlappDbContext’.’

The issue was (as the exception states) that I was disposing the context twice. To fix it, I simply removed the using statement outside GetContextAsync():

public async Task<IEnumerable<Tag>> FetchTagsAsync(CancellationToken cancellationToken = default)
{
    /*using*/ var context = await GetContextAsync(cancellationToken);
    
    return await context.Tags
        .AsNoTracking()
        .ToListAsync(cancellationToken);
}

Concurrency Issues on the DbContext

When clicking the AddDataSeries Button:

System.InvalidOperationException: ‘A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.’

Refactored the records back to class

I really wanted to use record, but this wasn’t the use case for it.

Refactoring Plapp.Persist

When I started watching this video series, I realised that I’ve been making the DataStore complicated. I can really cut down a lot of the boiler plate and use generics for my data access. This is a huge relief, and I can’t wait to get it done!

The refactoring is now done, but it seems I’ll be retracing some of my steps (“Fixing the database”, “Disposing Context and Dependency Injection” above). Could .AsNoTracking() be the answer?

Adding Data Points

Stuff that needs to be done:

  • CreateDataPointsViewModel
    • Keep track of user input
    • Undo actions
    • Persist the data
  • Create ViewModel implementations for the various data types
    • Decimal
    • Integer
    • Bool
    • Check
  • DataPoint Form
    • UI for data point creation
    • Depending on data type
  • Make sure the dataseries is updated when the points are added

Targeting Windows in Test projects

I can’t really explain why, but it’s neccessary to add -windows to the target framework for test projects (Ref: Error NETSDK1136).

<TargetFramework>net5.0-windows</TargetFramework>

Adding AutoMapper

Having gone back and forth with the data mapping functions over the course of this project, I’ve finally decided to try AutoMapper. Starting here.

Adding AutoMapper was a huge relivef, and it lead me down an interesting path of discovering additional Nuget packages to try. Added AutoFixture in order to auto-mock data more easily. It’s working fine with the ViewModels since they’re using mostly interfaces. Of note: I did struggle with Delegate types for a while, but all I was missing was this configuration detail:

new Fixture()
  .Customize(new AutoMoqCustomization { GenerateDelegates = true });

Adding animation to the LoadingPage

As a training exercise, I thought I’d add some animation on the LoadingPage. It was very easy to follow this article.

Next up, I’ll take a crack at Lottie. At first glance, it looks a bit proprietary (and sort of too good to be true), but heres to hoping!

Lottie works like a charm so far! Steps to add an animation to the project

  1. Download one that you like from lottiefiles (in JSON format)
  2. Drag&Drop the JSON into the ("Assets/Animations" or "Resources/Animations") folders and make sure the build action is correct ("AndroidAsset" or "BundleResource").
  3. Reference it in the ViewModel:
public string Animation { get; private set; }

public LoadingViewModel()
{
    
    Animation = Path.Combine("Animations","Pineapple.json");
}
  1. Bind it in the view:
new AnimationView
{
    AutoPlay = true,
    RepeatMode = RepeatMode.Infinite
}.Bind(AnimationView.AnimationProperty, nameof(VM.Animation));

Binding nested view models

new Slider()
.Bind(nameof(VM.Current) + '.' + nameof(VM.Current.Value))

I wonder if there’s a way to write an extension method in order to express the same thing with something like:

new Slider()
.Bind(RelativePath(VM.Current.Value))

Sketching the UI

First, some inspiration.

MediatR and beyond

I’ve made a major overhaul on Plapp’s architecture. Concerns feel incredibly separate. After adding MediatR, formulating business logic has been a breeze. Still, mapping between domain objects and view models is a bit of a pain. It feels like mapping from DTO to a view model shouldn’t go automatically, because the view model is so wired up, and with nested references, can end up with desynchronized State (two vms have the same Domain Id, but not the other way around).

It could be that the answer is to make mapping one-way (VM -> DTO), and to make view models process DTOs explicitly.

That said, MediatR has made it incredibly easy to add logging and validation. Because the infrastructure is separated from the components in the architecture, it’s so much easier to iterate on it.

ReactiveUI and Reactive Extensions

There’s a lot to take in, and it may be easiest to just start from scratch, because there’s a lot to change, and ReactiveUI promises to take care of navigation, and view creation.

Useful links to wrap my head around the reactive paradigm

Shell Navigation

Shell in Xamarin seems like the way forward when it comes to structuring the application.

Todo

  • Look into EFCore database migrations. Do I need it?

  • Visualize DataSeries
    • The topic view should be able to adjust the time span, which will affect all dataseries
    • Alternatives
  • Add a mechanism where ‘new’ DataPoints are created in the context of their data series (i.e. with data type + id filled in)

  • Create graceful animation handling in BasePage.OnAppearing and OnDisappearing

  • Sketch UI Layout

  • Add Localization (gettext
  • Transition to MAUI (blog post, repo)
  • User confirmation on delete (create a ViewModel which can be bound to a button?)
    • ConfirmActionViewModel ()
    • Topic
    • DataSeries
    • DataPoints
    • Tag (may need extra confirmation because it’s latteraly connected)
  • Reactive Extensions for more complex observer patterns.

  • Tests for the ViewModels
    • Prerequisites: Figure out the business logic
  • Test infrastructure
    • Navigation
    • ViewFactory
  • Add Transition Animations
  • Refactor Config
    • Add whole Connection String to config (not just the filename)

Inner Exceptions

Remember to Break on all exceptions when debugging!!

Create a data mapping layer

Goal: ViewModels should not have to depend on the DomainObjects directly, or at least not to have to call mapping functions explicitly.

Maybe it can be solved with a middleware around the data services, so the VMs will always be served with VMs, which they can use to hydrate themselves. It may be a good idea to prevent AutoMapper from instantiating any objects itself, and only be allowed to hydrate existing ones.

Right now, bugs are constantly popping up around the state of Models and their relations. Either new objects are created by AutoMapper, and EntityFramework is confused, or they’re being hydrated incompletely, and some model constraint is broken. Circular references have even caused stack overflow.

It could also be a good idea to relax the constraints on the Database tables. It should be possible to have “half finished” objects, but they should have limited features until they’re filled out.

I think maybe MediatR could be a good fit. This video has a good explanation on how to separate out business logic (ViewModels only holding state), and put middleware around it. It seems like a perfect fit, seeing as I don’t have much business logic yet, and I’m looking for somewhere to put middleware. It could help a lot with logging and exception handling.

Bugs

  • Expander does not give more Vertical space after it’s been expanded:
    • Example: TopicPage.descriptionExpander
  • Investigate if event subscription (example) can cause a memory leak.

Ideas

  • Add properties to a data series, and time stamps for when they change
    • E.g. A plant is standing in a window facing north. Time passes, data is gathered. The plant is moved to a window facing south. When looking at the time series, it will be observable when the plant was moved.