.Net Core

Workaround for the Dotnet Maui Tabs ‘OnAppearing’ event

Tabs in dotnet maui have an ‘OnAppearing’ event which you would expect to be called each time a tab is activated. This however is not the case and a possible workaround is to initialize data in a static manner and pass arguments using the ‘Arguments’-construct like shown in this post :

Tab1:

<!--?xml version="1.0" encoding="utf-8" ?-->
<contentpage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:class="MauiApp1.Pages.Tab1" xmlns:local="clr-namespace:MauiApp1.Data" title="Tab1">

	<contentpage.bindingcontext>
		<local:itemviewmodel>
			<x:arguments>
				<x:int32>10</x:int32>
			</x:arguments>
		</local:itemviewmodel>
	</contentpage.bindingcontext>

	<verticalstacklayout>
		<label text="Integers as argument">
		<collectionview x:name="collectionView" margin="5" itemtemplate="{StaticResource DataTemplate1}" itemssource="{Binding ItemCollection}" selectionmode="Single">
	</collectionview></label></verticalstacklayout>

</contentpage>

Tab2:

<!--?xml version="1.0" encoding="utf-8" ?-->
<contentpage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:class="MauiApp1.Pages.Tab2" xmlns:local="clr-namespace:MauiApp1.Data" title="Tab2">

	<contentpage.bindingcontext>
		<local:itemviewmodel>
			<x:arguments>
				<x:int32>20</x:int32>
			</x:arguments>
		</local:itemviewmodel>
	</contentpage.bindingcontext>

	<verticalstacklayout>
		<label text="Integers as argument">
		<collectionview x:name="collectionView" margin="5" itemtemplate="{StaticResource DataTemplate1}" itemssource="{Binding ItemCollection}" selectionmode="Single">
	</collectionview></label></verticalstacklayout>

</contentpage>

Tab3:

<!--?xml version="1.0" encoding="utf-8" ?-->
<contentpage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:class="MauiApp1.Pages.Tab3" xmlns:local="clr-namespace:MauiApp1.Data" title="Array of integers">

	<contentpage.bindingcontext>
		<local:itemviewmodel>
			<x:arguments>
				<x:array type="{x:Type x:Int32}">
					<x:int32>10</x:int32>
					<x:int32>11</x:int32>
					<x:int32>12</x:int32>
					<x:int32>13</x:int32>
					<x:int32>14</x:int32>
					<x:int32>15</x:int32>
				</x:array>
			</x:arguments>
		</local:itemviewmodel>
	</contentpage.bindingcontext>

	<verticalstacklayout>
		<label text="Array of integers as arguments">
		<collectionview x:name="collectionView" margin="5" itemtemplate="{StaticResource DataTemplate1}" itemssource="{Binding ItemCollection}" selectionmode="Single">
	</collectionview></label></verticalstacklayout>

</contentpage>

Where the Itemcollection looks like this:

public class ItemViewModel : INotifyPropertyChanged
{
	public ItemViewModel( )	{}

	public ItemViewModel( int categoryId)
	{
		this.ItemCollection = new ObservableCollection<item>( );

		for( int i = 0; i ‹ 4; i++)
		{
			this.ItemCollection.Add( new Item( ) { Name = $"V1-{categoryId}", Description = $"c{categoryId}/T1 rocks"});
		}
	}

	public ItemViewModel( int[] categoryIds)
	{
		this.ItemCollection = new ObservableCollection<item>( );

		for( int i = 0; i ‹ categoryIds.Length; i++ )
		{
			int categoryId = categoryIds[i];
			this.ItemCollection.Add( new Item( ) { Name = $"V1-{categoryId}", Description = $"c{categoryId}/T1 rocks"});
		}
	}

	public event PropertyChangedEventHandler PropertyChanged;

	protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
	{		
		ArgumentNullException.ThrowIfNull(e);
		PropertyChanged?.Invoke(this, e);
	}

	public ObservableCollection<item> ItemCollection	{ get; set; }
}

This will result in a nice flexible way of initialisation for tabs:

Integer argument
Integer argument
Array of integers as argument

An example project can be loaded from here.

A Multilevel Responsive Blazor-menu

When you generate a Blazor-server app or Webassembly the standard template generates a menu docked to the left which is responsive, but has some drawbacks :

  • It doesn’t support multiple levels
  • It depends on bootstrap which introduces additional complexity
  • Menu-items are hardcoded

Webapplications need a flexible way enabling multilevel-, responsive menus, the standard template is not flexible enough for this purpose, this post describes a solution to this problem. The approach taken consists of the following parts :

  1. A Component ‘ResponsiveMenuComponent’
  2. A stylesheet ‘menu.css’
  3. JQuery functions to toggle menus

Suppose our requirement is to generate a menu whos structure comes from an external source and simulates a webshop, the menustructure looks like this :

Home
Jeans -> Wide leg jeans
Jeans -> Straight jeans
Jeans -> Loose jeans
Shorts -> Sweet jersey shorts -> Jersey1
Shorts -> Sweet jersey shorts -> Jersey2
Shorts -> Denim shorts
Skirts
Blazors

The ResponsiveMenu-component has a property ‘MenuEntryCollection’ which is used to render the menu. With the ResponsiveMenu-component in place, the following creates the required menu-structure :

MainLayout.razor :

<ResponsiveMenuComponent Items=@Items />

@code {
public MenuEntryCollection	Items = new MenuEntryCollection( )
	{
	new MenuEntry( ) { Text = "Home",		Url = "/" }
,	new MenuEntry( ) { Text = "Jeans",		Url = "/jeans" 
	, SubItems = new MenuEntryCollection( )
		{
		new MenuEntry( ) { Text = "Wide leg jeans",	Url = "/wide_leg_jeans" }
	,	new MenuEntry( ) { Text = "Straight jeans",	Url = "/straight_jeans" }
	,	new MenuEntry( ) { Text = "Loose jeans",	Url = "/loose_jeans" }
}
}
,	new MenuEntry( ) { Text = "Shorts",		Url = "/shorts" 
	, SubItems = new MenuEntryCollection( )
		{
		new MenuEntry( ) { Text = "Sweet jersey shorts",	Url = "/sweet_jersey" 
		, SubItems = new MenuEntryCollection( )
			{
			new MenuEntry( ) { Text = "Jersey1",	Url = "/Jersey1" }
		,	new MenuEntry( ) { Text = "Jersey2",	Url = "/Jersey2" }
	}
	}
	,	new MenuEntry( ) { Text = "Denim shorts",			Url = "/demin_jersey" 	}
}
}
,	new MenuEntry( ) { Text = "Skirts",		Url = "/skirts" }
,	new MenuEntry( ) { Text = "Blazers",	Url = "/blazers" }
};
}

The ‘MenuEntryCollection’ and ‘MenuEntry’-classes:

public class MenuEntryCollection : System.Collections.Generic.List<MenuEntry>{ }

public class MenuEntry
{
	internal string Text { get; set; }
	internal string Url  { get; set; }

	internal MenuEntryCollection SubItems{ get; set; }
}

When the website is rendered for non-mobile devices, multilevel-items are expanded by css included in ‘menu.css’. Before blazor, if a user would select an item in the menu a postback would occur rebuilding the menu, in Blazor this is not the case causing the selected menu-item to be expanded. In a regular blazor app, it is collapsed using css switched when a user clicks a div, the ‘@onclick=”ToggleNavMenu’ construct. Doing this for a menu whos structure is unknown in advance would be very convenient, therefore this is done using jQuery declared inside _Host.cshtml together with some other needed functions:

<script type="text/javascript">
window.onBlazorReady = function() {
	$('.top-menu').not(".mobile").click(function () {
		$(this).find('ul > li').toggle();
	});

	$('.top-menu').not(".mobile").hover(function () {
		$(this).find('ul > li').show();
	}, function() {});

	$('.menu-toggle').click(function () {
		$(this).siblings('.top-menu.mobile').slideToggle('slow');
	});

	$('.top-menu.mobile .sublist-toggle').click(function () {
		$(this).siblings('.sublist').slideToggle('slow');
	});

	$('.top-menu.mobile .sf-with-ul').click(function () {
		$(this).siblings('.sublist').slideToggle('slow');
	});

	$('.top-menu.mobile li:not(:has(ul))').click(function () {
		$(this).closest('.top-menu.mobile').toggle();
	});
};
</script>

The ‘onBlazorReady’-function is called inside the ‘OnAfterRenderAsync’ inside ‘ResponsiveMenuComponent.razor’:

protected override Task OnAfterRenderAsync( bool firstRender )
{
  if( firstRender)
  {
    ValueTask valueTask = JsRuntime.InvokeVoidAsync("onBlazorReady");
  }

  return base.OnAfterRenderAsync( firstRender );
}

A running website using this component can be seen here, sources can be downloaded from github.

Notes:

  • The menu in component ResponsiveMenuComponent is generated in codebehind which disables css-isolation
  • The example given is Blazor-server but can also be used for Blazor-WASM
  • Menu items for the desktop are collapsed on hover using css, but there is no such thing as ‘hover’ on mobile devices. Because of this, we need two separate menus mobile and desktop

Specialized ViewComponents

Suppose you want to introduce an inheritance chain in ASP.Net Core to use some businesslogic in a baseclass :

namespace Foo.Core.Web
{
	using Microsoft.AspNetCore.Mvc;
	using System.Collections.Generic;

	public class CoolBaseViewComponentDetail : ViewComponent
	{
		protected List<string> getItems(int itemCount)
		{
			List<string> items = new List<string>();
			for (int i = 0; i < itemCount; i++)
			{
			  items.Add(string.Format("string {0}", i));
			}

			return (items);
		}
	}
 
	public class SpecializedViewComponentDetail : CoolBaseViewComponentDetail
	{
		public virtual IViewComponentResult Invoke(int numberOfItems)
		{
			List<string> items = getItems(numberOfItems);
			return (View(viewName: "~/Src/ViewComponentSpecialized.Detail.cshtml", model: items));
		}
	}
}

ViewComponentSpecialized.Detail renders like this :

@model System.Collections.Generic.List<string>
<ul>
    @foreach( string item in Model)
    {
        <li>@item</li>
    }
</ul>

If you invoke this viewcomponent you get an exception :

@await Component.InvokeAsync( name:"SpecializedViewComponentDetail", arguments:new { numberOfItems = 3 } )

InvalidOperationException: Could not find an 'Invoke' or 'InvokeAsync' method for the view component 'Foo.Core.Web.CoolBaseViewComponentDetail'.

ASP.Net core requires an ‘Invoke’-method on every ViewComponent-specialized class, even if it is not directly called. Solution is to introduce this method but to let it throw an exception when directly called:

namespace Foo.Core.Web
{
	using Microsoft.AspNetCore.Mvc;
	using System.Collections.Generic;
		
	public class CoolBaseViewComponentDetail : ViewComponent
	{
		public virtual IViewComponentResult Invoke(int numberOfItems)
		{
			throw new System.NotSupportedException("Specialized classes should implement this");
		}

		protected List<string> getItems(int itemCount)
		{
			List<string> items = new List<string>();
			for (int i = 0; i < itemCount; i++)
			{
			  items.Add(string.Format("string {0}", i));
			}

			return (items);
		}
	}
 
	public class SpecializedViewComponentDetail : CoolBaseViewComponentDetail
	{
		public virtual IViewComponentResult Invoke(int numberOfItems)
		{
			List<string> items = getItems(numberOfItems);
			return (View(viewName: "~/Src/ViewComponentSpecialized.Detail.cshtml", model: items));
		}
	}
}

Now the view renders:

o string 0
o string 1
o string 2

cheers, Stephan

Configuring Webservice endpoints in a .Net Core Webapplication

If you need to invoke a WCF-Soap compliant Webservice from an ASP.Net application, the endpoints for it are stored in the Web.Config:

<client>
    <bindings>
      <basicHttpBinding>
        <binding name="BasicHttpBinding_IService1" />
        <binding name="BasicHttpsBinding_IService1">
          <security mode="Transport" />
        </binding>
        <binding name="BasicHttpBinding_IService11" />
        <binding name="BasicHttpsBinding_IService11">
          <security mode="Transport" />
        </binding>
      </basicHttpBinding>
    </bindings>

  <endpoint address="http://sdeservice.azurewebsites.net/Service1.svc"
            binding="basicHttpBinding"
            bindingConfiguration="BasicHttpBinding_IService1"
            contract="ServiceReference1.IService1" 
            name="BasicHttpBinding_IService1" />

  <endpoint address="https://sdeservice.azurewebsites.net/Service1.svc"
            binding="basicHttpBinding"
            bindingConfiguration="BasicHttpsBinding_IService11"
            contract="ServiceReference2.IService1" 
            name="BasicHttpsBinding_IService1" />
/client>

If you need to invoke the same Webservice from an ASP.Net Core application, there is no Web.Config and you need to store the endpoints in an applicationSettings.json file. The following applicationSettings.json file introduces a section 'Service1' with some properties defining the Webservice to use:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Service1": {
    "Title": "Service1",
    "Name": "Very Cool Webservice",
    "Endpoint": "http://sdeservice.azurewebsites.net/Service1.svc",
    "SecureEndpoint": "https://sdeservice.azurewebsites.net/Service1.svc"
  },
  "AllowedHosts": "*"
}

There are a couple of ways to get this configuration in .Net Core 3.1 :

  1. Bind configuration in controller
  2. Bind configuration in Startup
  3. Dynamically read configuration

Bind configuration in controller

Any .Net Core application allows you to bind a configuration to a simple POCO-class, the following code binds an instance of class 'Service1Config' to properties 'Title', 'Name', 'Endpoint' and 'SecureEndpoint' from the 'Service1'-section. Your controller needs to have a constructor with an 'IConfiguration'-argument, ASP.Net Core will invoke it and you can use it to bind to a class. Then the endpoint as stored in the 'Endpoint'-attribute can be used at the endpoint of a Webservice-client as shown in the ‘Invoke’-method or ‘InvokeSecure’ to use https:

public class HomeController : Controller
{
  private Service1Config ConfigClass { get; set; }

  public HomeController( IConfiguration configuration)
  {
    this.ConfigClass = new Service1Config( );
    IConfigurationSection section = configuration.GetSection( "Service1");
    section.Bind( this.ConfigClass);
  }

  public IActionResult Invoke()
  {
    ServiceReference1.Service1Client client = new ServiceReference1.Service1Client( Service1Client.EndpointConfiguration.BasicHttpBinding_IService1);
    string endpoint = this.ConfigClass.Endpoint;
    client.Endpoint.Address = new EndpointAddress( endpoint);
    string result = this.Client.GetData( 3);
    return( View( ));
  }

  public IActionResult InvokeSecure()
  {
    ServiceReference1.Service1Client secureClient = new ServiceReference1.Service1Client( Service1Client.EndpointConfiguration.BasicHttpsBinding_IService1);
    string secureEndpoint = this.ConfigClass.SecureEndpoint;
    secureClient.Endpoint.Address = new EndpointAddress( secureEndpoint);
    string result = this.Client.GetData( 3);
    return( View( ));
  }
}

public class Service1Config
{
  public string Title		  { get; set; }
  public string Name		  { get; set; }
  public string Endpoint	  { get; set; }
  public string SecureEndpoint{ get; set; }
}

Bind configuration in Startup

When you need to have access to the configuration globally, you can do the same in your programs 'Startup.cs':

public class Startup
{
  internal Service1Config ConfigClass{ get; set; }

  public Startup( IConfiguration configuration)
  {
    this.ConfigClass= new Service1Config( );
    IConfigurationSection section = configuration.GetSection( "Service1");
    section.Bind( this.ConfigClass);
  }
}

Dynamically read configuration

If you need to dynamically read the same configuration, you can do so like this :

public class HomeController : Controller
{
  public IActionResult Index()
  {		
    string title = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("Service1")[ "Title"];
    string title = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("Service1")[ "Title"];
    string endpoint = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("Service1")[ "Endpoint"];
    string secureEndpoint = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("Service1")[ "SecureEndpoint"];
    return( View( ));
  }
}

Notes

  • If you add a reference to a Webservice, Visual Studio 2019 will add a 'connectedServices.json' file which also appears when you publish suggesting it stores a reference to the endpoint url used by a webservice-client. This is not the case, and the file can be safely removed from the publish.
  • If you generate a proxy for http, the generated proxy will contain a constructor without arguments
  • If you generate a proxy for http and https (examples are using that), the generated proxy will contain a constructor with an argument indicating the protocol to use, either http or https (BasicHttpBinding_IService1/BasicHttpsBinding_IService1)