A pleasant walk through computing

Comment for me? Send an email. I might even update the post!

.NET MAUI Progressing From a Default Project Part 1 - Adding the View Model

The Series

Level: Moderate

This series assumes the developer is familiar with .NET MAUI, the Model-View-ViewModel pattern, and has worked through one or more tutorials such as the excellent James Montemagno's Workshop.

Source: https://github.com/bladewolf55/net-maui-progression

The Problem .NET MAUI Solves

Cross-platform development is hard, usually requiring maintaining a project per platform. Any feature must be added to each code base. .NET MAUI uses a single project with the majority of the UI code using XAML, and the business code in C#. This code is then transpiled to the native platforms. The result is less maintenance, fewer errors, and reduced knowledge needed to build cross-platform applications.

(Brief) Getting Started: The Default App

These instructions assume running in Windows 10/11. You can follow the instructions below, which are brief and aren't intended as a beginner's guide. Or, better yet, use Microsoft's official documents.

The Project

  1. Install and open Visual Studio 2022 Preview
  2. Create a new project
  3. Filter for C# MAUI, or search for ".NET MAUI App"
  4. Create a .NET MAUI App using the defaults. Change the name to match mine if you want.

The Android Emulator

Important
You'll need to enable either Hyper-V or HAXM.

Note
I had trouble with Hyper-V the first time I tried .NET MAUI on my laptop even though it's supported, and had to use HAXM for a while. The problem went away when I did a clean install of Windows 11. Presumably, that would have been true in Windows 10.

  1. Choose Debug Target > Android Emulator
  2. Click to start the emulator. The Android Device Manager starts. Answer Yes to allow changes.
  3. You can accept the default Pixel 5 Android 12.0 (Level 31). However, I recommend canceling at this point and manually creating a Pixel 5 Android 12.1 (Level 32) or later. There are some behavior differences worth seeing, specifically in how the splash screen is treated.
  4. You don't need to explicitly start the emulator. Running the app will do that. So . . .
  5. Run the app!

This should start the emulator, then install and run the default app. On my machine, this takes a couple of minutes.

Clicking the button increases a counter.

Warning
As of 2022-06-20, using Android 12.1 as configured above, clicking the button doesn't display the number of clicks. I'm not sure what in the style sheets is preventing this, but you can work around it this way:

  1. Open Resources > Styles > Styles.xslt
  2. Add "MinimumWidthRequest". This forces a wide button.
<Style TargetType="Button">
    <Setter Property="MinimumWidthRequest" Value="200" />
    <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Primary}}" />

MVVM Basics

Great, we have a working app! And the .NET MAUI team have included lots of boilerplate to understand. But one thing not included is implementing the Model-View-ViewModel "MVVM" pattern.

We need to separate our code to improve maintenance and testing (more on testing in a later post!). One pattern for doing this is MVVM. Similar to Model-View-Controller (MVC), MVVM says,

  1. The Model contains our raw domain information and business behaviors, which may come from various data sources. (See Domain-Driven Design.)
  2. The View Model typically retrieves the Model via a service. The View Model then prepares the information for display.
  3. The View displays whatever's in the View Model.

The separation of concerns here is that

  1. The View doesn't know how the View Model gets its information.
  2. The View Model doesn't know how the Model gets its information.

This allows us to make changes to the View, View Model, and Model mostly independently. We strive for loose coupling.

An important aspect of MVVM is how to bind the View to the View Model. The user interacts with the View, for example clicking a button to increment a counter. This updates the data stored in the View Model (and possibly gets passed to the Model).

However, we can also update the values programmatically directly in the View Model, and we want those values to display in the View. This is two-way binding.

Creating the View Model

Right now, when we click the app's button it runs code in the MainPage.xaml.cs class. If this looks familiar to some of you1, it's because it's the same code-behind approach used in ASP.NET WebForms.

It works, but is too tightly coupled. Let's pull that simple functionality into a View Model.

  1. Add the NuGet package Microsoft.Toolkit.Mvvm.

    This is the same package as CommunityToolkit.Mvvm. Only the namespace differs.
    A reader kindly informed me that the Microsoft version of the toolkit should no longer be used. Instead, use the Community version.

  2. Add a folder named ViewModels, and a file named Counter.cs.

    Some developers append their view models with "VM" or "ViewModel". That's fine, but I don't, 'cause that's what namespaces are for!

  3. Add the following code
    using Microsoft.Toolkit.Mvvm.ComponentModel;
    using Microsoft.Toolkit.Mvvm.Input;
    
    namespace Maui.Progression.ViewModels;
    
    public partial class Counter : ObservableObject
    {
        [ObservableProperty]
        [AlsoNotifyChangeFor(nameof(CountText))]
        int count;
    
        public string CountText
        {
            get
            {
                string text = "Click me";
                if (count > 0)
                {
                    text = $"Clicked {count} " + (count == 1 ? "time" : "times");
                }            
                SemanticScreenReader.Announce(text);
                return text;
            }
        }
    
        [ICommand]
        void IncreaseCounter()
        {
            Count++;
        }
    }
    

The Microsoft.Toolkit.Mvvm package includes code generators to create all of the two-way binding code using the attributes. Here we've

  • Created an observable property named Count (the generator capitalizes the property name for us based on the backing field)
  • Created a read-only property named CountText. This is what we'll display.
  • Told the framework "when you notify of a change in Count, also notify there was a change in CountText."
  • Created a bindable command named IncreaseCounter to do the work.

Notes

  • Must be a partial class
  • Must inherit from ObservableObject
  • The Count property must be incremented, not the backing field.

Let's modify the MainPage view. Change the declarations.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Maui.Progression._01.MainPage"
             xmlns:ViewModels="clr-namespace:Maui.Progression._01.ViewModels"
             x:DataType="ViewModels:Counter"
             >

This code allows Intellisense to work. It doesn't perform the binding. That's done in the code-behind as shown later.

  • Adds the ViewModels namespace
  • Declares the view is of type Counter.

Change the button code.

            <Button
                Text="{Binding CountText}"
                SemanticProperties.Hint="Counts the number of times you click"
                Command="{Binding IncreaseCounterCommand}"
                HorizontalOptions="Center" />
  • Binds the button text to our CountText property
  • Binds the button click to the IncreaseCounter command.

    Important The toolkit code generator automatically appends "Command" to methods decorated with ICommand.

Finally, in the MainPage.xaml.cs code-behind, replace with this.

using Maui.Progression._01.ViewModels;

namespace Maui.Progression._01;

public partial class MainPage : ContentPage
{
	public MainPage()
	{
		InitializeComponent();
		BindingContext = new Counter();
	}
}

This is what actually binds the View to the View Model.

Running the code should behave as before.

Tricks

Building for multiple platforms takes awhile. If you're willing to develop mostly for Windows, and check progress on Android/iOS/Mac occasionally, you can comment out the mobile targets.

<PropertyGroup>
    <!--<TargetFrameworks>net6.0-android;net6.0-ios;net6.0-maccatalyst</TargetFrameworks>-->
    <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net6.0-windows10.0.19041.0</TargetFrameworks>

Troubleshooting

If you get a message "The namespace already contains a definition for 'Counter'," double check you defined Counter as a partial class. You may then have to close Visual Studio, delete the project's bin and obj folders, restart Visual Studio, and build the project.

Sometimes I've seen the MVVM Toolkit generators/code get "stuck," probably due to some files being cached. Even if I comment out most of the class code, the error doesn't go away until I restart Visual Studio.

In some cases, I've cleaned up the VS generated documents. (But I don't know if this matters.)

  1. Open %LocalAppData%\Temp\VSGeneratedDocuments
  2. Delete all the folders

AND, if you're told you can't delete files because they're locked!!

Important
This is probably the most useful troubleshooting step I've found. When you close Visual Studio, it does not close adb.exe, which is used by the Android emulator. I'm betting this will be corrected in the future.

  1. Close Visual Studio
  2. Close any open Android Emulator and the Android Device Manager
  3. Open Task Manager (Ctrl+Shift+Esc)
  4. Open the Details tab
  5. End the adb.exe process

AND IF THAT DOESN'T WORK!!!

Restart and try clearing those files again. And delete the project's obj/bin again. Sheesh!

If you get this error

Argument 1: cannot convert from 'System.ComponentModel.PropertyChangedEventArgs' to 'int'

double check your View Model class is inheriting from ObservableObject, not BindableObject.

Wrap Up

We've actually done quite a bit here to implement the MVVM pattern.

  1. Created a View Model with bindable properties and commands
  2. Updated the View to use the View Model
  3. Bound the two together

Next up: unit testing!

Resources


  1. The ancient ones

How to Hide Markdown Breadcrumbs in VS Code

Here's a quick one. If you're using Visual Studio Code as your Markdown editor and work with larger documents, such as a daily journal, VS Code's default behavior of showing breadcrumbs might be a performance drag.

To turn off header-level breadcrumbs when working with just Markdown files:

  1. View > Command Palette > the JSON User settings > Preferences: Open Settings (JSON)
  2. Add this setting
    "[markdown]": {
        "breadcrumbs.showStrings": false
    },
    

If you want to disable breadcrumbs for all languages, use this setting:

"breadcrumbs.enabled": false

Happy writing!

Home-Job Balance: How I "End" My Job Day

Previously on Home-Job Balance . . .

In my previous post, I submitted that a big step toward improving work/life balance is to replace that phrase with "home-job balance." Doing so corrects several problems with the current phrase, and brings our attention where it often belongs: home.

I'm going to answer the questions I asked in that post and give some tips on how I "close up shop" for the day, which I can do at any time during the day.

When am I at home vs at the job?

Job
It's startlingly easier for me to identify when and where I'm on the job than at home.

  • When I'm performing tasks specifically for my company or my company's client (I work for a consulting company.)
  • When I'm reading company and client messages (Slack, email).
  • When I'm getting company/client notifications (meetings, messages, tasks).
  • At my computer with certain folders and applications open, such as client code repositories, Visual Studio, Azure DevOps.
  • Reading subject articles to complete my tasks.
  • Reading general articles/books for my career. This includes reading blog posts like the one I'm writing!
  • I feel on the job if I'm talking about my job to family and friends with my attention on solving job problems, not just describing what I do.
  • Likewise, I'm on the job if I'm dwelling on my job when I don't have to be.

Home
It'd be a mistake to decide I feel at home when I'm not doing my job.

Being home can't be "the absense of job."

Here are some ways I feel at home.

  • Working on house projects, especially when they're nice to do instead of "must do." I had to clean the garage door rails because the door kept (and, sigh, keeps) sticking. I crafted and installed a little shelf across the kitchen window for my wife's herbs. I felt at home with both, and enjoyed the second more.
  • Being in the living room with my wife and dog. It often doesn't matter what I'm doing. They're close and they matter.

The above involve being physically at home. But home doesn't have to be a place. A house isn't always a home. Some other ways I feel at home are,

  • Reading a book. The less it's related to my career, the more at home I feel. Fiction brings me home because I grew up entering the worlds of imagination.
  • Watching TV or movies. Like books, these take me out of my job and into other lives. Note that I do these on my computer.
  • Walking my neighborhood. I'm lucky to live somewhere I enjoy.
  • Exercising, especially either being instructed or leading a class.
  • Having earnest and playful conversations with family and friends.

Keeping my job separate when it uses the same place and tools

I do my job in my home office at my laptop. I also do home stuff in my home office on the same laptop. It's not productive for me to maintain two different laptops. Here's what I do to cue myself "this is the job, Dude."

Different browsers and/or profiles
I use Brave for my home browsing and Edge for my work browsing.

In Edge, I have two job profiles: my company and my client. To make switching between those two easier, I loaded my company profile and pinned that instance to my Windows task bar. Then I loaded my client profile and pinned that instance.

The result is it's faster and easier for me to open Edge with the needed bookmarks and security context in place.

Notice I put Home first?

Clearly separate job folders
I have a high-level job folder, and no personal projects go in that. Recently, I made another change I love. I have separate high-level Git repository folders for my job and personal projects.

C:\source <= ONLY client repos
|_project 1
|_project 2
C:\users\charl\source\repos <= ONLY personal repos
|_project 1
|_project 2

UPDATE 20220126: Reader Avesh Jain sent me the following nice suggestion.

One tip I'd like to mention is using virtual desktops. I keep a virtual desktop for personal apps and browsers, and one for each client/project. I pin common applications so they appear on all virtual desktops (e.g., OneNote).

Leaving the job for the day

I have a loose ritual I can do at any time to signal to myself "I'm done with my job for the day" or "done for now."

  1. Close job-specific browsers
    I close my job Edge browsers. But what if I have a couple dozen browser tabs open for a job project? I use a cool extension like Qlearly to save them all, and can reopen when I'm on the job again.

  2. Close job-specific apps
    Some apps I only use for my job, for example Slack. Slack "closes" to the notification area. I right-click that icon and Quit the app. Bam, Slack, you no longer control me!

  3. Close File Explorer showing job folders
    Not minimize. Close. I add friction to looking at my job's work.

    Confession: I actually use the terrific FreeCommander XE paid version to manage my files. My job folder tabs are in the left pane. I close those tabs, or close the app, or at least minimize it.

  4. Close any windows showing job-related documents
    For me, this can be: Visual Studio, VS Code, Word, Excel, Balsamiq, LINQPad . . . you get the point.

    I know. In your mind you're thinking, "But I just need to open them all again and pick up where I left off. It's so much easier to leave them open."

    That's true. It's easier. That's the point. You're making it easy to stay on the job. Instead, make it easy to be at home.

  5. Say, "I'm going home"
    This sounds silly. It isn't, and it takes one second. Try it.

  6. Close my laptop lid
    If I've had a really tough day.

  7. Walk away from my computer
    I was surprised how much difference it makes to physically go to another room where my computer isn't. I'll go back to it later on my own terms.

  8. Do something that makes me feel at home
    You know, from that list I made.

Maybe I'll open my computer again fifteen minutes later to shop for new bed sheets. Fine. I've given myself the end-of-job cue and a break I deserve right now.

Final Thoughts: Notifications

What and who are you putting first all the time? Have you made yourself "always available"?

By default, lots of computer and phone apps have their notification turned on. Schedule yourself some self-care time to learn your app settings, have a heart-to-heart with yourself, and turn off whatever you can.

For example: Slack/Teams/Discord users, ask yourself, "Which channels are urgent, if any?" I hope for you the initial answer isn't "all of them"! I set up Slack so I only get notified on a couple of channels and if I get a direct mention. When I quit Slack on my computer, I know I'll still get emergency notifications on my phone.

When you're on the job, put it first. When you're at home, put it first.