Tips to avoid getting scammed online.

The most common way people get scammed is by social engineering. I learned a few simple scam-prevention techniques from my co-hosts on the podcast Security This Week, where we teach security topics through the lens of current hacks. On the podcast, I'm the dumb guy asking questions, the other two are red-hat security professionals.

If you get an email or a text from what looks like a company that you pay money to on a regular basis, and they offer a link you can click to do whatever they are asking you to do, don't click it.

If it's an email, log on to the official website, open your account settings, and check to see if there are any notifications for you. If you don't see anything there, you're probably okay. You can confirm with the company by sending them an email or using their contact page. If it's a text, a Facebook message, or any other kind of instant message, don't click on it.

You might get an email from one of your credit card companies that has a number you should call right away. Don't call it. Instead, call the number on the back of your card and ask them if they sent you this email.

If you get a Facebook message from one of your friends with a request that seems out of character, like "Hey, Carl. I'm stuck in Miami because I got ripped off and I need $500 for a plane ticket home" - Don't answer it. Contact your friend by another means and ask them if they sent you a message.

Also, sometimes you'll get Facebook messages from friends you haven't heard from in a while, and the message is just something like "how are you?" or "hi..." they have probably been hacked. Again, contact them using another method, and confirm.

If you get an email or a message from a friend that has an attachment and the words "check this out!" or some other non-descript request, do not click it. This is how Ransomware gets installed.

The best way to confirm the identity of a friend online is to ask them a question that only they can answer, or if that seems too weird, just ask them to call you. Even if a scammer has your number, it won't be your friend on the other line.

Another tip is to NEVER scan a QR code in a public location that doesn't look official. Anyone can create a QR image from a URL, even if that URL points to a website that can infect your phone. Don't ever scan a QR code printed on a sheet of paper and taped to the wall, a bus, a billboard, or any other public property.

Wifi routers are another problem. You need to make sure your WIFI router is always up to date with the current firmware. I have one that automatically updates itself.

Also, you should have two WIFI networks, one that your computers connect to using a strong password, and another one, a GUEST network, that has no password, and is not connected to any computers or printers. That's the one you should use for your household devices (NEST, etc) and give to your guests.

Build MAUI Apps in an Azure VM

I love using a cloud-based virtual machine as my developer machine. A moment’s thought will reveal why. A VM is isolated. You can set up a developer environment on a per-project basis. You can access it from anywhere, untethering yourself from your office chair. You can also turn it off when not using to save money.

As great as that sounds, and it is, I have never used a VM (at least in Azure) for mobile development. It’s hard to find the right configuration for an Android emulator to work. Besides, I would rather use my device to test while I’m developing.

In this article I’ll show you how to install the latest MAUI preview with .NET 6 RC1 and Visual Studio 2022 Preview. I’ll also give a resource that will allow you to develop using your local Android device, even though Visual Studio is running in the cloud.

I’m not covering iOS devices in this blog post because I could not get them working using this workflow. Might be just me.

You can watch a video of me building this solution from start to finish at https://youtu.be/Pmr5nDJ1Q7w

Step 1: Create a new Azure VM

For this, I chose the “Standard D8s v3 (8 vcpus, 32 GiB memory)” SKU, which would cost about $280/month if you ran it 24/7. If you’re only using it for 8 hours a day the cost would be less than $100/month.

Step 2: Install Visual Studio 2022 Preview

You can download the Visual Studio Installer for VS2022 from https://visualstudio.microsoft.com/vs/preview/ Follow the instructions to install from https://docs.microsoft.com/en-us/dotnet/maui/get-started/installation

Let’s go over the critical install steps:

First, make sure you check off ASP.NET and web development, Universal Windows Platform development, and Mobile development with .NET.

Next, in the Installation details > Mobile development with .NET section of the installation window, check the .NET MAUI (Preview) optional workload, as it is NOT selected by default.

Step 3: Install requirements for Android devices.

First, install Microsoft OpenJDK version 11 from https://docs.microsoft.com/en-us/java/openjdk/download.

Next, follow the instructions at https://docs.microsoft.com/en-us/dotnet/maui/get-started/installation to install the Android 12 (API 31) SDK.

Step 4: Install Usb for Remote Desktop

This is the secret sauce. Download and install USB for Remote Desktop from https://www.usb-over-network.com/usb-for-remote-desktop.html.

You must install two apps: Server, which you install in your VM, and Workstation, which you install on your local machine.

They have a free trial, but the cost for a single developer is only 12 bucks a month. If you plan to use a VM for regular mobile work, this is money well spent.

Once installed, connect to your VM with RDP, but make sure you add your Android device to the list of local resources.

To accomplish this, run Remote Desktop Connection, and click the Show Options dropdown

Select the Local Resources tab, then click the More button.

Expand Other supported Plug and Play (PnP) devices and select your device from the list.

Step 5: Install your Android device’s USB driver

Download and install your Android device’s USB Driver in the VM

If you use a Samsung phone, you can install the USB driver from https://developer.samsung.com/mobile/android-usb-driver.html

Be careful. There are lots of unofficial sites offering plenty of clickbait. Make sure you get yours from the source website.

STEP 6: Connect to VM and register your Android Device

Once connected to your VM, expand the Show hidden icons button in the toolbar, and then right-click on the FabulaTech USB for Remote Desktop icon

Select your device from the list presented to you.

Step 7: Build and test your first MAUI app

Follow steps 1 through 4 from https://docs.microsoft.com/en-us/dotnet/maui/get-started/first-app?pivots=windows to build your first MAUI app. It is important to wait patiently until all of the dependencies have been recognized.

Step 8: Select the Android Platform and your local device.

If you wait long enough, Visual Studio should see your device and set it as the default target of deployment. If it doesn’t see your device, make sure it is selected by USB Remote Desktop as per Step 6.

This dropdown is how you select not only the device but the platform that you’re going to deploy to and run.

Step 9: Run the app

You must be patient! Go get a cup of coffee. It may take a while. Don’t stop it before it has a chance to complete. Wait until it gets past the .NET logo splash screen.

Yes, Hot Reload will probably not work due to the latency, but you get to see your app on your phone even though you’re developing in the cloud.

MVVM in Blazor!

mvvm.jpg

Blazor’s robust component model makes MVVM easy peasy. In these two episodes of BlazorTrain - a YouTube weekly by Carl Franklin - you’ll learn the WHY and how of using ViewModels in Blazor.

Episode 24 shows you how to convert an existing Blazor CRUD form into MVVM.

Episode 25 shows advanced MVVM patterns for more real-world scenarios.

For a list of all episodes go to https://blazortrain.com

Sponsored by DevExpress, offering professional components for Blazor.

Playing Audio Files in a Blazor Application

With a little JavaScript Interop you can play mp3 files in your Blazor application.

The first step is to gather a few mp3 files you can play in your app. Remember, these will be playing in the browser, so keep the size of the file in mind. You can download my demo audio files here.

Next, create a new Blazor app with Visual Studio 2019. You can use the free Community Edition if you like.

Next, create a sounds folder under the wwwroot folder, and copy your mp3 files to it. For each file, make sure the Build Action property is set to Content, and the Copy to Output Directory property is set to Copy if newer.

audio_demo_shot1.png

For our demo, we’re going to hijack the Index.razor file, replacing it with the following:

@page "/"
@inject IJSRuntime js;

<h1>Play MP3 Files</h1>

<button class="btn btn-primary" @onclick="PlayAudioFile1">Duh</button>
<span>&nbsp;</span>
<button class="btn btn-primary" @onclick="PlayAudioFile2">Dwayne</button>

<audio id="player">
    <source id="playerSource" src="" />
</audio>

@code
{
    async Task PlayAudioFile1()
    {
        await js.InvokeVoidAsync("PlayAudioFile", "/sounds/duh.mp3");
    }

    async Task PlayAudioFile2()
    {
        await js.InvokeVoidAsync("PlayAudioFile", "/sounds/dwayne.mp3");

    }
}

All we’ve done here is set up a couple buttons to execute code that calls into a JavaScript function called "PlayAudioFile”, which we have yet to write. Let’s do that now.

Open your startup HTML file. In Blazor Server this is Pages/_Host.cshtml and for Blazor WebAssembly it’s wwwroot/Index.html.

Add the following to it, just below the script tag that loads the blazor JavaScript file:

    <script>
        window.PlayAudioFile = (src) => {
            var audio = document.getElementById('player');
            if (audio != null) {
                var audioSource = document.getElementById('playerSource');
                if (audioSource != null) {
                    audioSource.src = src;
                    audio.load();
                    audio.play();
                }
            }
        }
    </script>

This code finds your audio player, sets the source file, loads it, and plays it.

Go ahead and try it.

Play Multiple Audio Files Simultaneously

You’ll notice that if you click the first button, and then the second button immediately, the second mp3 file cuts off the first one. You can fix this in a rather inelegant, yet functional, way.

Create another audio tag for the second mp3 so you can play two simultaneously. More files? Just add more tags. You’ll have to modify your code to handle the change.

Change the contents of Index.razor to this:

@page "/"
@inject IJSRuntime js;

<h1>Play MP3 Files</h1>

<button class="btn btn-primary" @onclick="PlayAudioFile1">Duh</button>
<span>&nbsp;</span>
<button class="btn btn-primary" @onclick="PlayAudioFile2">Dwayne</button>

<audio id="player1">
    <source id="playerSource1" src="" />
</audio>
<audio id="player2">
    <source id="playerSource2" src="" />
</audio>

@code
{
    async Task PlayAudioFile1()
    {
        await js.InvokeVoidAsync("PlayAudioFile", "1", "/sounds/duh.mp3");
    }

    async Task PlayAudioFile2()
    {
        await js.InvokeVoidAsync("PlayAudioFile", "2", "/sounds/dwayne.mp3");

    }
}

We’ve added a second audio tag, and named them “player1” and “player2”. We’ve also updated the names of the source elements to “playerSource1” and “playerSource2”.

We’ve also modified our JavaScript calls to pass the player number as a string.

Modify your PlayAudioFile JavaScript function to handle the change:

    <script>
        window.PlayAudioFile = (playerNumber, src) => {
            var audio = document.getElementById('player' + playerNumber);
            if (audio != null) {
                var audioSource = document.getElementById('playerSource' + playerNumber);
                if (audioSource != null) {
                    audioSource.src = src;
                    audio.load();
                    audio.play();
                }
            }
        }
    </script>

Now you have two “channels” with which to play audio files. Great.

Load and play audio dynamically!

Wouldn’t it be great if we didn’t have to provision these audio controls before using them? With a little more JavaScript we can create them on the fly, load the audio file, and play them. Now the audio files will play completely asynchronously.

Change your Index.razor contents back to the first version minus the audio element:

@page "/"
@inject IJSRuntime js;

<h1>Play MP3 Files</h1>

<button class="btn btn-primary" @onclick="PlayAudioFile1">Duh</button>
<span>&nbsp;</span>
<button class="btn btn-primary" @onclick="PlayAudioFile2">Dwayne</button>

@code
{
    async Task PlayAudioFile1()
    {
        await js.InvokeVoidAsync("PlayAudioFile", "/sounds/duh.mp3");
    }

    async Task PlayAudioFile2()
    {
        await js.InvokeVoidAsync("PlayAudioFile", "/sounds/dwayne.mp3");

    }
}

Now, let’s change the JavaScript function to create the audio tag, set the properties, add it to the document, load the audio, and play.

        window.PlayAudioFile = (src) => {
            var sound      = document.createElement('audio');
            sound.src      = src;
            sound.type     = 'audio/mpeg';
            document.body.appendChild(sound);
            sound.load();
            sound.play();
        }

Enjoy!

Blazor Shopping Cart Sample using Local Storage to Persist State

This little demo is based on a Gist written by Steve Sanderson where he shows how to persist state in the browser using sessionStorage. His demo shows how to persist the counter value but I wanted to try something a bit more real-world, a very simple shopping cart, and also to use localStorage rather than sessionStorage. You can download the project here.

Here’s how it works. Select an item from the store (in this case a list). The details of that item are shown below the list. Enter a quantity and add it to your cart. The contents of your cart are shown below that, including the grand total. Now refresh the browser (or close and re-open). If you refresh within 30 seconds, your cart is still there. If you wait longer than 30 seconds before refreshing the browser, the cart data is no longer there.

My top level class is called Cart. Every time you update the cart, the contents are serialized and stored in the browser’s local storage using a not-as-yet-released package, Microsoft.AspNetCore.ProtectedBrowserStorage.

I have added a couple fields to the Cart class to support expiration:

    public DateTime LastAccessed { get; set; }
    public int TimeToLiveInSeconds { get; set; } = 30; // default

Whenever the Cart is persisted to localStorage, the LastAccessed time stamp is saved. When loading the cart from localStorage, a check is made to see if the cart has expired based on the LastAccessed and TimeToLiveInSeconds properties. I set it to 30 seconds by default so you could easily test it without having to wait around for a long period of time to elapse.

The Code

I created a Blazor Server project in Visual Studio 2019 called BlazorStateTest1.

Let’s start with the three models:

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
    }
    
    public class CartProduct
    {
        public int Quantity { get; set; }
        public Product Product { get; set; }

        public decimal Total
        {
            get
            {
                return Product.Price * Quantity;
            }
        }
    }
    
    public class Cart 
    {
        public List<CartProduct> Items { get; set; } = new List<CartProduct>();

        public Decimal Total
        {
            get
            {
                decimal total = (decimal)0.0;
                foreach (var item in Items)
                {
                    total += item.Total;
                }
                return total;
            }
        }
        public DateTime LastAccessed { get; set; }
        public int TimeToLiveInSeconds { get; set; } = 30; // default
    }    

At the very bottom of the food chain is Product. Simple enough. Then we have a CartProduct which represents a Product and Quantity, and has a Total property that multiplies the product price by the quantity.

Finally, the Cart object has a list of CartProducts and the aforementioned properties to support expiration. This is a greatly simplified shopping cart. The focus here should be on the persistence patterns.

Following Steve’s instruction, I added the Microsoft.AspNetCore.ProtectedBrowserStorage package to the project.

Next, I added this line to my ConfigureServices method in Startup.cs:

    services.AddProtectedBrowserStorage();

Next, I created a component called CartStateProvider.razor to handle loading and saving the cart:

@inject ProtectedLocalStorage ProtectedLocalStore

@if (hasLoaded)
{
    <CascadingValue Value="@this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {

    [Parameter] 
    public RenderFragment ChildContent { get; set; }

    public Cart ShoppingCart { get; set; }

    bool hasLoaded;

    protected override async Task OnParametersSetAsync()
    {
        // Retrieve the Shopping Cart from Local Storage (in the browser)
        ShoppingCart = await ProtectedLocalStore.GetAsync<Cart>("MyShoppingCart");

        // If the Cart is not there or empty...
        if (ShoppingCart == null || ShoppingCart.Items.Count == 0)
        {
            // Create a new Cart
            ShoppingCart = new Cart();
        }
        else
        {
            // otherwise, check to see if the Cart has expired (default is 30 seconds)
            if (DateTime.Now > ShoppingCart.LastAccessed.AddSeconds(ShoppingCart.TimeToLiveInSeconds))          
            {
                // Expired. Create a new cart.
                ShoppingCart = new Cart();
            }
        }
        ShoppingCart.LastAccessed = DateTime.Now;
        hasLoaded = true;
    }

    public async Task SaveChangesAsync() 
    {
        // Set the time stamp to current time and save to local storage (in the browser).
        ShoppingCart.LastAccessed = DateTime.Now;
        await ProtectedLocalStore.SetAsync("MyShoppingCart", ShoppingCart);
    }
}

This component has an instance of Cart (ShoppingCart) which it loads and saves to localStorage.

Rather than just instantiating a CartStateProvider in index, I took Steve’s advice and made it a Cascading Parameter so it could be used everywhere. To do that, I wrapped the Router in App.razor in an instance of CartStateProvider:

<BlazorStateTest1.Shared.CartStateProvider>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</BlazorStateTest1.Shared.CartStateProvider>

Finally, we can use it in the Index page (the default page for Blazor projects):

@page "/"

@if (AllProducts != null)
{
    // This demo is based on a technique Steve Sanderson Showed in the following Github Gist:
    // https://gist.github.com/SteveSandersonMS/ba16f6bb6934842d78c89ab5314f4b56

    <h2>Select an item</h2>
    //Display the list of products. Call ProductSelected when one is selected
    <select size="4" style="width:100%;" @onchange="ProductSelected">
        @foreach (var product in AllProducts)
        {
            <option value="@product.Id.ToString()">@product.Name</option>
        }
    </select>
    <br />

    // Show the selected product
    @if (SelectedProduct != null && ShowItem == true)
    {
        <div style="padding:1vw;background-color:lightgray;">
            <table cellpadding="5" cellspacing="5">
                <tr>
                    <td align="right" valign="top"><strong>Name:</strong></td>
                    <td align="left" valign="top">@SelectedProduct.Name</td>
                </tr>
                <tr>
                    <td align="right" valign="top"><strong>Description:</strong></td>
                    <td align="left" valign="top">@SelectedProduct.Description</td>
                </tr>
                <tr>
                    <td align="right" valign="top"><strong>Price:</strong></td>
                    <td align="left" valign="top">$@SelectedProduct.Price</td>
                </tr>
                <tr>
                    <td align="right" valign="top"><strong>Add To Cart:</strong></td>
                    <td align="left" valign="top">
                        Quantity:
                        <input @bind="Quantity" />
                        <button @onclick="AddToCart">Add</button>
                    </td>
                </tr>
            </table>
        </div>
    }

    // Show the cart contents if there are items in it.
    @if (CartStateProvider != null && CartStateProvider.ShoppingCart.Items.Count > 0)
    {
        <br />
        <h3>Your Cart:</h3>
        <h4>Total: $@CartStateProvider.ShoppingCart.Total</h4>
        <table cellpadding="5" cellspacing="5">
            @foreach (var item in CartStateProvider.ShoppingCart.Items)
            {
                <tr>
                    <td colspan="2">
                        <hr />
                    </td>
                </tr>
                <tr>
                    <td align="right" valign="top"><strong>Name:</strong></td>
                    <td align="left" valign="top">@item.Product.Name</td>
                </tr>
                <tr>
                    <td align="right" valign="top"><strong>Description:</strong></td>
                    <td align="left" valign="top">@item.Product.Description</td>
                </tr>
                <tr>
                    <td align="right" valign="top"><strong>Price:</strong></td>
                    <td align="left" valign="top">$@item.Product.Price</td>
                </tr>
                <tr>
                    <td align="right" valign="top"><strong>Quantity:</strong></td>
                    <td align="left" valign="top">@item.Quantity</td>
                </tr>
                <tr>
                    <td align="right" valign="top"><strong>Total:</strong></td>
                    <td align="left" valign="top">$@item.Total</td>
                </tr>
                <tr>
                    <td colspan="2">
                        @*Clicking this button passes the item so we can remove it*@
                        <button @onclick="@(() => RemoveItem(@item))">Remove</button>
                    </td>
                </tr>
            }
        </table>
        <br />
        <h4>Total: $@CartStateProvider.ShoppingCart.Total</h4>
    }

}

@code {

    // Cascading Parameters and Values flow down the entire component tree
    [CascadingParameter] CartStateProvider CartStateProvider { get; set; }

    bool ShowItem = false;
    string Quantity = "1";
    List<Product> AllProducts;
    Product SelectedProduct;

    void ProductSelected(ChangeEventArgs args)
    {
        // User clicked on an item in the list.
        // Show the product and give them an option to add to cart.
        SelectedProduct = (from x in AllProducts
                           where x.Id == Convert.ToInt32(args.Value)
                           select x).First();
        Quantity = "1";
        ShowItem = true;
    }

    async Task AddToCart()
    {
        // Create a new item for the shopping cart
        var item = new CartProduct
        {
            Product = SelectedProduct,
            Quantity = Convert.ToInt32(Quantity)
        };
        // Add it to the cart
        CartStateProvider.ShoppingCart.Items.Add(item);
        // Save to local storage
        await CartStateProvider.SaveChangesAsync();
        // Stop displaying the selected item
        ShowItem = false;
    }

    async Task RemoveItem(CartProduct Item)
    {
        // User clicked a Remove button to remove an item from the cart.
        CartStateProvider.ShoppingCart.Items.Remove(Item);
        // Update the cart - save to localstorage
        await CartStateProvider.SaveChangesAsync();
    }

    protected override void OnInitialized()
    {
        // Create the products we can purchase
        AllProducts = new List<Product>();

        AllProducts.Add(new Product
        {
            Id = 1,
            Name = "1 lb. Bag of Yirgacheffe Coffee Beans",
            Description = "Yirgacheffe is a rich Ethiopian coffee that will poke your eye out",
            Price = (decimal)10.99
        });

        AllProducts.Add(new Product
        {
            Id = 2,
            Name = "Tablet Show Coffee Mug",
            Description = "Back by popular demand for a limited time, the long-coveted Tablet Show Coffee Mug",
            Price = (decimal)4.99
        });
    }
}

Everything starts at the OnInitialized method. That’s where I create two products and add them to the AllProducts list.

Now look at the top of the page. If the AllProducts list is loaded, we show the products in a select element.

When an item is selected, the ProductSelected method fires, and the SelectedProduct is set.

Next, we have logic that shows the product info if SelectedProduct isn’t null and the boolean ShowItem is set to true. That table includes an input element for the quantity and an Add button that calls the AddToCart method when clicked.

The AddToCart method then creates a CartProduct, adds it to the CartStateProvider.ShoppingCart, and persists it.

Next, if the CartStateProvider.ShoppingCart isn’t null and actually contains items, it is shown below, again in a table element. Each item has a Remove button that when clicked calls RemoveItem, which removes the item from the cart and again saves the cart.

Take a look at the expiry code in the CartStateProvider’s OnParametersSetAsync method. This happens when the page loads and the cart gets instantiated. This is the critical check right here:

if (DateTime.Now > ShoppingCart.LastAccessed.AddSeconds(ShoppingCart.TimeToLiveInSeconds)) 

If this statement is true, then the cart has expired and a new empty cart is returned.

Summary

LocalStorage is good for small amounts of data - Steve says storing a few K of data there is probably okay - but you should be thinking about server-side storage of state if the amount of data going over the wire is hindering performance. For small bits, however, localStorage is the perfect solution.

Is Blazor For Everyone?

Someone recently asked me why I was so enthusiastic about Blazor. This person didn’t like the programming experience as much as their JavaScript framework.

So, here’s the deal. If you are happy with your stack, your tribe, rock on. Blazor obviously isn’t for you. So, who is it for?

Blazor is for enterprise developers who are tasked with writing internal applications, B2B, and B2C apps. Yes, many enterprise devs have embraced JavaScript, but many MANY more have not. They don’t like plumbing code. They want to get the job done as quickly and efficiently as possible.

Many of these devs thought Silverlight was going to be their path to web-delivered apps. Everywhere I go I have been pummeled with comments like “Blazor is just Silverlight vNext. Why should we trust Microsoft not to kill it?”

This sentiment is understandable, but to conflate Blazor’s future with Silverlight’s past is misinformed.

Silverlight was a casualty of the browser plug-in model. At the time Flash was a very popular plugin for UI, and it was wreaking havoc with iOS devices: iPads and iPhones. Steve Jobs famously announced that Safari would no longer support plugins because of the security concerns over Flash. When developers realized that their Silverlight apps wouldn’t run on the most popular up-and-coming mobile platform, it took the wind out of Silverlight’s sails.

By contrast, Blazor runs in the browser on WebAssembly, which is supported by every major browser (Except IE 11), even Safari, even mobile browsers. WebAssembly is here to stay. It’s a standard that has already been adopted. Think of it as an operating system for running compiled code in the browser in the same sandbox that JavaScript runs in.

What makes Blazor unique from all of the other languages that run on WebAssembly is the runtime. The mono CLR is compiled to WebAssembly. But it’s a runtime. Any .NET Standard 2.0 code will run there. Other languages are compiled directly to WebAssembly. As far as I know there aren’t any other runtimes for WebAssembly.

Silverlight tangent aside, enterprise developers have been waiting for a web programming model that doesn’t drown them in plumbing code. Blazor’s binding model is easy to understand. The component model is elegant. The eventing model is even less ceremonious than standard C#. With tool vendors like DevExpress providing controls that do the heavy lifting, these developers have a reason to be delighted.

So, rock on my JavaScript friends. Rock on. Maybe Blazor isn’t for you. I happen to know that there’s an army of developers out there who are breathing a sigh of relief.

Carl

Reuse Blazor WASM UI in Blazor Server

So, you’ve decided to use Blazor, but you’re not sure which hosting model to use: Client (WebAssembly) or Server (SignalR-based). Wouldn’t it be great if you could write one version of your app that can run as either a WebAssembly or Server-based Blazor app? With a few tweaks, and by following a few design guidelines, you can.

One of the side benefits of this technique is that you can debug your client-side Blazor apps in Visual Studio, something that can’t be done at the time of this writing. That’s because all the code in your Web Assembly project is actually running on the server.

I first learned of this pattern at NDC London 2020 in Steve Sanderson’s Blazor talk. The secret sauce is to add a reference from a Server-Side Blazor app to a Client-Side Blazor app, and then register the App component in the Server as a reference to the Client-side App component.

Follow these steps and you’ll see for yourself how this could be a game-changer. The complete solution is available for download here.

Prerequisites

Start by following the instructions in the Microsoft Docs for getting started with Blazor. Install the WebAssembly template as described.

Create a new Blazor WebAssembly App named BlazorWasm

Make sure to check off “ASP.NET Core hosted”

Name your app BlazorWasm

Name your app BlazorWasm

Select Blazor WebAssembly App and check off “ASP.NET Core hosted”

Select Blazor WebAssembly App and check off “ASP.NET Core hosted”

Add a Blazor Server App to the solution named BlazorServer

Name the project BlazorServer

Name the project BlazorServer

This time, select Blazor Server App

This time, select Blazor Server App

Add a reference from BlazorServer to BlazorWasm.Client and BlazorWasm.Shared

In the BlazorServer project, right click on the project name.

From the BlazorServer project, select Add -&gt; Reference

From the BlazorServer project, select Add -> Reference

Then select the BlazorWasm.Client and BlazorWasm.Shared projects from the Projects menu.

Select both BlazorWasm.Client and BlazorWasm.Shared

Select both BlazorWasm.Client and BlazorWasm.Shared

Delete Pages in Server app

Now the fun begins. Delete Counter.razor, Error.razor, FetchData.razor, and Index.razor from the BlazorServer project.

Delete these four pages

Delete these four pages

Change the Server App Component to Client App Component

In the BlazorServer project, open _Hosts.cshtml, locate the <app> tag, and change the component definition from this:

    <component type="typeof(App)" render-mode="ServerPrerendered" />

to this

    <component type="typeof(BlazorWasm.Client.App)" render-mode="Server" />
Change &lt;app&gt; component to reference the client

Change <app> component to reference the client

Set Multiple Start Projects

In order to test this, we have to run the WASM server, which loads the WebAssembly client, and the BlazorServer Server-side project as well.

Right-click on the Solution at the top of the Solution Explorer, and Select Properties. Under Common Properties -> Startup Project select “Multiple Startup Projects”, then set the Action for BlazorServer and BlazorWasm.Server to “Start”

image-8.jpg

Rebuild All and Run

You will see two tabs: BlazorServer and BlazorWasm. Both look exactly the same. Try using the Counter page from both. Try going to the Fetch Data page on both. Uh oh. Exception!

The Server app doesn’t know about HttpClient.

The Server app doesn’t know about HttpClient.

This Exception illustrates how you will need to design your app from now on. The HttpClient class is available in the BlazorWasm.Client project by default, but not in the BlazorServer project.

Here’s the rule:

In order to achieve reusability between these two projects, they must each have references to the same external resources: projects, packages, scripts, etc.

So, we have to add a scoped HttpClient service to the BlazorServer project. Open the startup.cs file and add the following to the ConfigureServices method:

    services.AddScoped<System.Net.Http.HttpClient>();

We’re not done yet. Take a look at the FetchData.razor file in BlazorWasm.Client. It’s trying to access an API controller that’s in the same url scope. In order for this to work in both client and server, we have to be more explicit.

In the BlazorWasm.Server project, expand Properties, and then click on launchSettings.json. Copy the SSL port. We’re going to need that.

image-10.jpg

Change the controller URL in FetchData.razor

In the BlazorWasm.Client project, open FetchData.razor under the Pages folder, and change the URL from "WeatherForecast" to “https://localhost:[port]/WeatherForecast” replacing “[port]” with the SSL port from launchSerttings.json:

@code{
    private WeatherForecast[] forecasts;
    protected override async Task OnInitializedAsync()
    {
        string url = "https://localhost:44306/WeatherForecast";
        forecasts = await Http.GetJsonAsync<WeatherForecast[]>(url);
    }
} 

Now when you run the app, you can access the Weather Data from both the Server-side and Client-side blazor apps.

If you use this pattern, your server will use the exact same patterns and code that the client uses. That may not always be appropriate. So, make sure you can commit to an architecture that puts the client design first.

Other Resources

Come see me in a city near you on my Blazor Roadshow world tour

Attend my one-day online Blazor workshop to dig into Blazor big time.

Check out DevExpress Blazor Components, which are currently free.

Happy coding!

Logging SignalR Traffic in Server-Side Blazor

Server-Side Blazor is indeed magical due to a hidden communications system keeping a persistent connection between the browser and the server: SignalR. SignalR establishes a circuit for every client and keeps a server-side model of the DOM in memory. The app sends messages when interactions happen in the browser, such as a button click. The server receives those messages and executes whatever code it needs to, such as a button click handler, applies it to the graph, calculates the difference, and sends messages back to the app which updates the DOM. It’s brilliant, really. However, being hidden, it piques our curiosity as to what’s being sent and received.

As I mentioned, this SignalR system is hidden from you as a developer. You can’t just open up the browser tools and watch the traffic. However, with a little tweaking, you can listen in on the conversation and you might be surprised at how little data is being transmitted.

The secret sauce is in how you configure the logging provider and filters. Let me show you how.

Create a new Server-Side Blazor app in Visual Studio 2019 and in the program.cs file change the CreateHostBuilder call to this:

        public static IHostBuilder CreateHostBuilder(string[] args) =>
          Host.CreateDefaultBuilder(args)
             .ConfigureLogging(logging =>
             {
                 logging.ClearProviders();
                 logging.AddConsole();
                 logging.AddFilter(
                   "Microsoft.AspNetCore.SignalR", LogLevel.Trace);
                 logging.AddFilter(
                   "Microsoft.AspNetCore.Http.Connections",
                   LogLevel.Trace);
             })
             .ConfigureWebHostDefaults(webBuilder =>
             {
                 webBuilder.UseStartup<Startup>();
             });

Run the app and open the Output debug window.

Select the application from the Output window’s “Show Output From” dropdown.

As you use the app, you can watch the traffic in the Output window.

Select your application in the Output window’s “Show Output From” dropdown list.

Select your application in the Output window’s “Show Output From” dropdown list.

Let’s take a look at the output. The first thing you see is IIS Express registering the app. No big whoop.

Starting IIS Express ...
Successfully registered URL "http://localhost:50115/" for site "BlazorApp1" application "/"
Successfully registered URL "https://localhost:44389/" for site "BlazorApp1" application "/"
Registration completed for site "BlazorApp1"
IIS Express is running.

Next you see services for json and blazorpack being registered.

dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubProtocolResolver[1]
      Registered SignalR Protocol: json, implemented by Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubProtocolResolver[1]
      Registered SignalR Protocol: blazorpack, implemented by Microsoft.AspNetCore.Components.Server.BlazorPack.BlazorPackHubProtocol.

Next you see the hub methods Blazor uses bing bound:

trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[9]
      'ComponentHub' hub method 'StartCircuit' is bound.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[9]
      'ComponentHub' hub method 'ConnectCircuit' is bound.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[9]
      'ComponentHub' hub method 'BeginInvokeDotNetFromJS' is bound.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[9]
      'ComponentHub' hub method 'EndInvokeJSFromDotNet' is bound.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[9]
      'ComponentHub' hub method 'DispatchBrowserEvent' is bound.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[9]
      'ComponentHub' hub method 'OnRenderCompleted' is bound.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[9]
      'ComponentHub' hub method 'OnLocationChanged' is bound.

Next, we see the “heartbeat” being established. If you sit and do nothing, you’ll see the browser “ping” the server to keep the connection alive.

trce: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionManager[9]
      Starting connection heartbeat.

After a few lines saying that the app has started you see the actual WebSocket being opened with the browser.

dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionManager[1]
      New connection SYQgFSqfV_gwlp9aOtZ-FQ created.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionDispatcher[10]
      Sending negotiation response.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionDispatcher[4]
      Establishing new connection.
dbug: Microsoft.AspNetCore.SignalR.HubConnectionHandler[5]
      OnConnectedAsync started.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[1]
      Socket opened using Sub-Protocol: '(null)'.

Next, a little handshake. I don’t claim to know exactly what’s being said here, but 38 bytes are received using a “hub protocol” defined in blazorpack. Blazorpack is an internal layer that provides access to the hub methods and provides serialization/deserialization services, etc.

trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[9]
      Message received. Type: Text, size: 38, EndOfMessage: True.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubProtocolResolver[2]
      Found protocol implementation for requested protocol: blazorpack.
dbug: Microsoft.AspNetCore.SignalR.HubConnectionContext[1]
      Completed connection handshake. Using HubProtocol 'blazorpack'.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[11]
      Sending payload: 3 bytes.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[9]
      Message received. Type: Binary, size: 491, EndOfMessage: True.

Next, the “StartCircuit” method is invoked. It’s a bit of gobbledygook, but this is where things actually get interesting.

dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "0", Target: "StartCircuit", Arguments: [ https://localhost:44389/, https://localhost:44389/, [] ], StreamIds: [  ] }.

Here I’m showing the log entries that were generated when I clicked on the “Fetch Data” page:

trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[9]
      Message received. Type: Binary, size: 353, EndOfMessage: True.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "DispatchBrowserEvent", Arguments: [ ,  ], StreamIds: [  ] }.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[11]
      Sending payload: 169 bytes.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[7]
      InvocationId (null): Sending result of type 'System.Void'.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "OnLocationChanged", Arguments: [ https://localhost:44389/fetchdata, True ], StreamIds: [  ] }.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[9]
      Message received. Type: Binary, size: 27, EndOfMessage: True.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[11]
      Sending payload: 3528 bytes.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[11]
      Sending payload: 133 bytes.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[11]
      Sending payload: 140 bytes.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[7]
      InvocationId (null): Sending result of type 'System.Void'.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "OnRenderCompleted", Arguments: [ 7,  ], StreamIds: [  ] }.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[7]
      InvocationId (null): Sending result of type 'System.Void'.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[9]
      Message received. Type: Binary, size: 27, EndOfMessage: True.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "OnRenderCompleted", Arguments: [ 8,  ], StreamIds: [  ] }.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[7]
      InvocationId (null): Sending result of type 'System.Void'.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[9]
      Message received. Type: Binary, size: 27, EndOfMessage: True.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "OnRenderCompleted", Arguments: [ 9,  ], StreamIds: [  ] }.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[7]
      InvocationId (null): Sending result of type 'System.Void'.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[9]
      Message received. Type: Binary, size: 27, EndOfMessage: True.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "OnRenderCompleted", Arguments: [ 10,  ], StreamIds: [  ] }.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[7]
      InvocationId (null): Sending result of type 'System.Void'.

It looks like the browser is telling Blazor that the mouse was clicked at an X of 106 and a Y of 202. It wraps this in a “DispatchBrowserEvent” message.

Next an “OnLocationChanged” message is sent passing the url https://localhost:44389/fetchdata

That’s a bit different than standard REST traffic, isn’t it? That’s the point of server-side Blazor. It’s not a REST app. All the communication happens via SignalR, even simple navigation. Wow.

Four more messages are sent, which result in “OnRenderCompleted” actions. All in all, about 4K of data is sent, including the weather data itself.

Let’s watch what happens when we click the counter button on the Counter page. To measure this, go to the Counter page, clear the output window, and click the button. Here’s what you see.

trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[9]
      Message received. Type: Binary, size: 292, EndOfMessage: True.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "DispatchBrowserEvent", Arguments: [ ,  ], StreamIds: [  ] }.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[11]
      Sending payload: 148 bytes.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[7]
      InvocationId (null): Sending result of type 'System.Void'.
trce: Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport[9]
      Message received. Type: Binary, size: 27, EndOfMessage: True.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "OnRenderCompleted", Arguments: [ 7,  ], StreamIds: [  ] }.
trce: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[7]
      InvocationId (null): Sending result of type 'System.Void'.

Not much at all being sent here. I counted about 500 bytes from what I can see in the log.

This stuff is fascinating to me. I don’t pretend to know what exactly is being said, but the conversation is definitely terse.

Get a Windows 10 Pro PC for $160!

I recently put together a small network that required a couple very lightweight PCs running Windows 10. That’s when I stumbled across this little gem, the W5 Pro Mini PC which I picked up on Amazon for only $160!

The W5 Pro Mini PC is suprisingly poweful for less than $200!

The W5 Pro Mini PC is suprisingly poweful for less than $200!

What you get

  • Intel Atom Z8350 Processor (1.44 GHz)

  • Powered by Micro USB

  • Windows 10 Pro x64 Preinstalled

  • 4GB DDR RAM

  • 64GB eMMC Storage

  • Supports a Micro SD card up to 128GB

  • Built in 2.4G/5G AC WiFi and Bluetooth 4.2

  • Intel HD graphics

  • Video supports 4K HD

  • Two USB ports (one 2.0 and one 3.0)

Streaming Video

As a smart TV (access to all the services) it does a good job of keeping up with streaming video. It’s not as smooth as my MacBook but you can’t beat it for the money!

Travel PC

This is a great PC to take with you when you travel if using your hotel TV is acceptable. Just throw a keyboard and mouse in your suitcase.

Installation

Follow these simple steps and you’ll be up and running in no time!

Initial Setup

It’s very easy. Connect a keyboard and mouse via USB, plug it into a TV or monitor, power it up, and follow the prompts to set up Windows 10 Professional x64.

Check for Updates

Whenever I install Windows I immediately go get all the updates and make sure that the update schedule works and that I don’t have to babysit it.

Type Check for Updates in the Windows Search Bar found in the lower-left of the screen. Follow all of the instructions and don’t do anything else until the updates are all installed.

updates.png

Turn on Auto Login Feature

This is another step I take for every PC in my house. Since I’m not worried about someone else logging into it, I turn on the auto-login feature, so when the machine boots up it automatically logs me into Windows.

To enable this, type netplwiz in the Windows Search bar.

auto-login.png

Deselect the checkbox next to “Users must enter a user name and password to use this computer.” Enter your username and password. Hit Apply and then OK. That’s it.

Allow Remote Access

This is a nice option if you don’t want to dedicate a monitor, mouse, and keyboard to the PC. In my case, I have two of these running in a box, so I can’t really plug them into a monitor. Remote Access is a nice feature that allows me to connect remotely.

First step: Name your PC

Type This PC in the Windows Search bar, then right-click on the icon that says “This PC” in the File Explorer, and then select “Properties”. You’ll see a window like this:

This PC Properties

This PC Properties

I have named my PC MANTLE. To change your PC name, click the “Change Settings” link next to the Computer name. Pick a name you’ll remember.

Next Step: Allow Remote Access

To allow remote access, type "Remote Desktop Settings” into the Windows Search Bar and turn on Remote Acccess:

Remote Access allows you to connect to this PC without a dedicated monitor, mouse, and keyboard.

Remote Access allows you to connect to this PC without a dedicated monitor, mouse, and keyboard.

Extras!

These are some items that I added to my PC. I chose them because of the positive reviews on Amazon. They work well, and they are affordable.

BYEASY 4 Port USB 3.0 Hub ($12.99)

This was a no-brainer. The ByEasy Hub allows me to connect a total of four USB 3 devices and is completely USB-powered. Done.

$13 Hub. Big Bang for the Buck

$13 Hub. Big Bang for the Buck

Optimal Shop USB 2.0 External Sound Card ($14.59)

If you need 3.5mm audio inputs and outputs, I recommend this little gem. It’s totally USB powered, it has a line input, a very important feature for me, and it’s lightweight. It also has surround sound outputs, but I don’t use them in this project.

usbaudio.jpg

The best part? for a whopping total of $187.57 I have a decent PC with 4 USB 3 jacks, a line input, and surround sound support. You just can’t beat that… for now!

INotifyPropertyChanged behavior in Blazor Components

By design, when you build a server-side Blazor app, and you pass parameters to a component when those parameters change inside the component the changes are not reflected in the host. With a little hack, we can make that happen. It’s INotifyPropertyChanged all over again!

This gem came from my online 8-hour Blazor Workshop. You can purchase the video and materials or sign up for the next class.

In Visual Studio 2019 (or higher) create a new Blazor app called NotifyComponentDemo.,

Add a new class called Customer with two string properties: Name and Email.

Right-click on the Pages folder and select Add / New Item.

Select Web from the list on the left and then Razor Comopnent from the list.on the right.

Name the component Editor and replace with the following:

<div style="background-color:lightgray;">
    <div style="padding:1vw;">
        Name: <br />
        <input @bind="@Customer.Name" @bind:event="oninput" />
        <br/><br/>
        Email: <br />
        <input @bind="@Customer.Email" @bind:event="oninput" />
    </div>
</div>

@code {

    [Parameter]
    public Customer Customer { get; set; }

}

Now open Index.razor from the Pages folder and replace wiht the following:

@page "/"
<Editor Customer="Customer" />
<br />
<br />
Name: @Customer.Name
<br />
Email: @Customer.Email
<br />


@code {

    Customer Customer = new Customer();

    protected override void OnInitialized()
    {
        Customer = new Customer {
            Name = "Ben Goofin",
            Email = "ben@bengoofin.com"
        };
    }
}

Our component accepts a customer object as a parameter. It allow you to edit the name and email properties.

Press F5 and run. One might expect that as you edit the values, your Index page would show the changes as you type, especially since we are using the @bind:event=oninput syntax.

Not so, and this is by design.

In order to make this happen we have to first capture the keystrokes in the input boxes. Change the input tags to the following:

        Name: <br />
        <input @bind="@Customer.Name" 
               @bind:event="oninput" @onkeyup="KeyPressed" />
        <br/><br/>
        Email: <br />
        <input @bind="@Customer.Email" 
               @bind:event="oninput" @onkeyup="KeyPressed"  />

Now we need to handle that keypress event, but what should we do when a key is pressed?

We need to add an event handler to notify the host (Index.razor) that the state of the component (namely the Customer object) has changed.

Add the following code to the code block in the component:

    [Parameter]
    public EventCallback<string> ComponentDataUpdated { get; set; }

    async Task KeyPressed(KeyboardEventArgs args)
    {
        await ComponentDataUpdated.InvokeAsync("");
    }

Go back to the host (Index.razor) and modify as follows:

@page "/"

<Editor Customer="Customer" ComponentDataUpdated="ComponentUpdated" />
   
<br />
<br />
Name: @Customer.Name
<br />
Email: @Customer.Email
<br />


@code {

    Customer Customer = new Customer();

    protected override void OnInitialized()
    {
        Customer = new Customer {
            Name = "Ben Goofin",
            Email = "ben@bengoofin.com"
        };
    }

    void ComponentUpdated(string args)
    {
        StateHasChanged();    
    }
}

So, on every key press our component raises the ComponentUpdated event, which we handle in the host page in a like-named method, ComponentUpdated. The string argument is ignored but necessary.

All our code does is call StateHasChanged() and magically, Blazor updates the UI in the host that displays properties of the Customer object.

Voila'

Building a reusable generic template component for server-side Blazor

The goal is to develop a little bit of UI that can be reused in different Blazor projects or with different data. The component knows nothing about the class it’s working with. The Type is supplied by a generic parameter.

This component was developed for my Server-Side Blazor Workshop but I’m making it freely available because it answers so many fundamental questions about Blazor.

Download the code with sample project here.

Imagine you have a master list of some type of item, and you want the user to be able to select one or more of them for whatever reason. Here is what the UI looks like in our demo:

objectpicker.jpg

The component name is ObjectPicker. Here is the complete source:

@typeparam TItem

<table style="width:100%">
    <tr>
        <td style="width:45%;" valign="top">
            All @ItemTypePlural<br />
        </td>
        <td style="width:10%;" valign="top">
            <span>&nbsp;</span>
        </td>
        <td style="width:45%;" valign="top">
            Selected @ItemTypePlural<br />
        </td>
    </tr>
    <tr>
        <td style="width:45%;" valign="top">
            <select @ondblclick="ItemDblClickedFromAllItems"
                    @onchange="ItemSelectedFromAllItems"
                              size="10" style="width:100%;">
                @foreach (var Item in AllItems)
                {
                    if (@ItemValue(Item) == @ItemValue(SelectedItem))
                    {
                        <option selected value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                    else
                    {
                        <option value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                }
            </select>
        </td>
        <td style="width:10%;" valign="top">
            <button @onclick="AddSelectedItem"
                              type="button"
                              disabled="@AddSelectedItemButtonDisabled"
                              style="width:100%;">
                &gt;
            </button><br />
            <button @onclick="@AddAllItems"
                              type="button"
                              style="width:100%;">
                &gt;&gt;
            </button><br />
            <button @onclick="@RemoveSelectedItem"
                              type="button"
                              disabled="@RemoveSelectedItemButtonDisabled"
                              style="width:100%;">
                &lt;
            </button><br />
            <button @onclick="@RemoveAllItems"
                              type="button"
                              style="width:100%;">
                &lt;&lt;
            </button><br />
        </td>
        <td style="width:45%;" valign="top">
            <select @ondblclick="ItemDblClickedFromSelectedItems"
                    @onchange="ItemSelectedFromSelectedItems"
                              size="10" style="width:100%;">
                @foreach (var Item in SelectedItems)
                {
                    if (@ItemValue(Item) == @ItemValue(SelectedItem))
                    {
                        <option selected value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                    else
                    {
                        <option value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                }
            </select>
        </td>
    </tr>
</table>

@code {

    [Parameter]
    public string ItemType { get; set; }

    [Parameter]
    public string ItemTypePlural { get; set; }

    [Parameter]
    public string TextPropertyName { get; set; }

    [Parameter]
    public string ValuePropertyName { get; set; }

    [Parameter]
    public List<TItem> AllItems { get; set; }

    [Parameter]
    public List<TItem> SelectedItems { get; set; }

    [Parameter]
    public EventCallback<string> ComponentUpdated { get; set; }

    TItem SelectedItem { get; set; }

    bool AddSelectedItemButtonDisabled = true;
    bool RemoveSelectedItemButtonDisabled = true;

    private string ItemValue(TItem Item)
    {
        return Item.GetType()
         .GetProperty(ValuePropertyName)
         .GetValue(Item, null)
         .ToString();
    }

    private string ItemText(TItem Item)
    {
        return Item.GetType()
         .GetProperty(TextPropertyName)
         .GetValue(Item, null)
         .ToString();
    }

    protected override void OnParametersSet()
    {
        if (AllItems.Count > 0)
        {
            // remove the items that exist in SelectedItems
            foreach (var item in SelectedItems)
            {
                var id = item.GetType()
                   .GetProperty(ValuePropertyName)
                   .GetValue(item, null)
                   .ToString();


                var ItemFromAllItems =
                 (from x in AllItems
                  where x.GetType()
                    .GetProperty(ValuePropertyName)
                    .GetValue(x, null)
                    .ToString() == id
                  select x).FirstOrDefault();

                if (ItemFromAllItems != null)
                {
                    AllItems.Remove(ItemFromAllItems);
                }
            }
        }
        if (AllItems.Count > 0)
        {
            SelectedItem = AllItems.First();
        }
        else if (SelectedItems.Count > 0)
        {
            SelectedItem = SelectedItems.First();
        }
        UpdateButtonEnabledStates();
    }

    void ItemDblClickedFromAllItems()
    {
        AddSelectedItem();
    }

    void ItemDblClickedFromSelectedItems()
    {
        RemoveSelectedItem();
    }

    void ItemSelectedFromAllItems(ChangeEventArgs args)
    {
        SelectedItem =
         (from x in AllItems
          where x.GetType()
            .GetProperty(ValuePropertyName)
            .GetValue(x, null)
            .ToString() == args.Value.ToString()
          select x).FirstOrDefault();

        UpdateButtonEnabledStates();
    }

    void UpdateButtonEnabledStates()
    {
        AddSelectedItemButtonDisabled = !AllItems.Contains(SelectedItem);
        RemoveSelectedItemButtonDisabled = !SelectedItems.Contains(SelectedItem);
    }

    void AddAllItems()
    {
        foreach (var Item in AllItems.ToArray())
        {
            SelectedItems.Add(Item);
        }
        if (SelectedItems.Count > 0)
        {
            SelectedItem = SelectedItems.First();
        }
        AllItems.Clear();
        UpdateButtonEnabledStates();
        ComponentUpdated.InvokeAsync("").Wait();
    }

    void RemoveAllItems()
    {
        foreach (var Item in SelectedItems.ToArray())
        {
            AllItems.Add(Item);
        }
        if (AllItems.Count > 0)
        {
            SelectedItem = AllItems.First();
        }
        SelectedItems.Clear();
        UpdateButtonEnabledStates();
        ComponentUpdated.InvokeAsync("").Wait();
    }

    void AddSelectedItem()
    {
        if ((from x in SelectedItems
             where ItemValue(x) == ItemValue(SelectedItem)
             select x).FirstOrDefault() == null)
        {
            SelectedItems.Add(SelectedItem);
            AllItems.Remove(SelectedItem);
            UpdateButtonEnabledStates();
            ComponentUpdated.InvokeAsync("").Wait();
        }
    }

    void RemoveSelectedItem()
    {
        if ((from x in AllItems
             where ItemValue(x) == ItemValue(SelectedItem)
             select x).FirstOrDefault() == null)
        {
            AllItems.Add(SelectedItem);
            SelectedItems.Remove(SelectedItem);
            UpdateButtonEnabledStates();
            ComponentUpdated.InvokeAsync("").Wait();
        }
    }

    void ItemSelectedFromSelectedItems(ChangeEventArgs args)
    {
        SelectedItem =
         (from x in SelectedItems
          where x.GetType()
            .GetProperty(ValuePropertyName)
            .GetValue(x, null)
            .ToString() == args.Value.ToString()
          select x
         ).FirstOrDefault();
        UpdateButtonEnabledStates();
    }
}

Let’s take a look at the very first line:

@typeparam TItem

This is how we can define a data type passed in as parameters. Look at the AllItems parameter:

    [Parameter] public List<TItem> AllItems { get; set; }

When you create an instance of this component, you pass in a list of whatever you want.

The component has to have a way to access the properties we need to use, one for the text that gets displayed, and another for the value (Id) that identifies the object. We expose these property names as parameters:

    [Parameter]
    public string TextPropertyName { get; set; }

    [Parameter]
    public string ValuePropertyName { get; set; }

Take a look at how we instantiate this component:

<ObjectPicker @ref="InstrumentPicker"
              ItemType="Instrument"
              ItemTypePlural="Instruments"
              AllItems="Instruments"
              SelectedItems="SelectedInstruments"
              TextPropertyName="Name"
              ValuePropertyName="InstrumentId"
              ComponentUpdated = "ComponentUpdated"
              />

ItemType and ItemTypePlural are strings that define what the user is looking at.

TextPropertyName is the name of the property in the class that will be displayed

ValuePropertyName is the name of the Id property, in this case InstrumentId

AllItems is a list of all the items that show up on the left.

SelectedItems is a list of all the items on the right

In the ObjectPicker itself, we have this field:

TItem SelectedItem { get; set; }

Whenever an item is selected in either of the <select> boxes, this is the item that was clicked on.

Let’s look at the <select> on the left, which shows AllItems:

            <select @ondblclick="ItemDblClickedFromAllItems"
                    @onchange="ItemSelectedFromAllItems"
                              size="10" style="width:100%;">
                @foreach (var Item in AllItems)
                {
                    if (@ItemValue(Item) == @ItemValue(SelectedItem))
                    {
                        <option selected value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                    else
                    {
                        <option value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                }
            </select>

Notice we’re getting the display text and value from two methods: ItemValue and ItemText:

    private string ItemValue(TItem Item)
    {
        return Item.GetType()
            .GetProperty(ValuePropertyName)
            .GetValue(Item, null)
            .ToString();
    }

    private string ItemText(TItem Item)
    {
        return Item.GetType()
            .GetProperty(TextPropertyName)
            .GetValue(Item, null)
            .ToString();
    }

We use a bit of reflection to get the values of these properties. We also use the same technique in a LINQ query to match the selected item based on the value of the value property. This method is called when an item is selected:

    void ItemSelectedFromAllItems(ChangeEventArgs args)
    {
        SelectedItem =
        (from x in AllItems
         where x.GetType()
             .GetProperty(ValuePropertyName)
             .GetValue(x, null)
             .ToString() == args.Value.ToString()
         select x).FirstOrDefault();

        UpdateButtonEnabledStates();
    }

The rest of it is just enabling and disabling buttons, and moving items from one side to the other.

The host app can use the SelectedItems list to update the actual objects, entities, or what have you.

Want more Blazor? How about a one-day workshop online? Details at http://blazor.appvnext.com

Carl

Xamarin Forms gets Mac Desktop and more!

I was just watching the latest Xamarin Show with James Montemagno and was pleasantly surprised to see that Xamarin.Forms 2.4.0 Service Release 2 (September, 2017) added support for Mac Desktop apps. Of course, you have to use Visual Studio for the Mac, but that's completely understandable. In this episode, David Ortinau showed the gallery sample app running as a Mac desktop app. Very very cool.

formsMacOs.png

They also made performance improvements by tightening up the number of UI layers required for basic controls. These improvements are especially noticeable with lists.

On top of that, they announced support for .NET Standard 2.0 (and therefore lower)

These features are new. Think of them as alpha.