A place for me to put reminders, tips, tricks and 'gotchas' about software development. It's all public in case others might find it useful.
Wednesday, August 30, 2017
Visual Studio 2017 Web Debug Start Page
It had nothing to do with file or pool permissions, or what account your were debugging under, or what sort of ASP.NET project it was, or anything else I could discover. Strangely enough, after clicking OK the debugging actually started (without a UI), so luckily I could temporarily ignore the problem and continue developing my web services.
After a couple of weeks this problem became so irritating that I had to have a fresh look to defeat it. By luck, or a hunch, or desperation, or who knows what ... in solution explorer I right-clicked the index.htm file and selected "Browse With ..." and a small dialog appears. In the dialog I notice that the selected item in the Browsers list is Microsoft Edge (Default).
Changing the default to Internet Explorer solves the problem.
So here we are again ... despite my decades of wide IT experience, it takes me hours of puzzling and frustration spread out over many weeks to defeat a problem that has a trivial fix. Extensive web searches on this problem revealed no useful hints whatsoever.
IMPORTANT NOTE (a few days later)
A regular correspondent in the ozdotnet forum pointed out to me that your choice of default browser is easily visible in the Visual Studio Standard Toolbar ... if you have it visible. I personally hide all toolbars on Visual Studio and most other products that I use regularly, because I have of course memorised all of the keyboard shortcuts and prefer not to waste space and visual clutter on toolbars. Sadly, in this case, being a smartarse backfired and prevented me from quickly noticing the underlying cause of the problem.
Predictably, a short comical exchange resulted in the forum around the claim that "real programmers don't use toolbars".
Tuesday, August 22, 2017
log4net UDP Listener
In my realistic scenario there are dozens of applications of various types running on two servers in an office network, and all the log4net sections of the config files specify:
<remoteAddress value="224.9.9.9"/>
<remotePort value="9999"/>
The address 224.9.9.9 is in the global multicast address scope so anyone within the broadcast horizon can listen-in.
But exactly how can a .NET listener client application somewhere on the local network listen to the logging broadcasts from the server machines? I fumbled around for a while getting no broadcasts because I didn't realise you had to "join" the multicast group. When I saw the JoinMulticastGroup method in the MSDN documentation I suddenly uncovered the missing link to make my C# code work. In skeleton form the required code is like the following.
var ep = new IPEndPoint(IPAddress.Any, 9999); var client = new UdpClient(ep); client.JoinMulticastGroup(IPAddress.Parse("224.9.9.9")); while(true) { UdpReceiveResult result = await client.ReceiveAsync(); }
Put the ReceiveAsync in a loop that suits your coding style. There is no elegant way to detect when a client is closed to end your loop. Some part of your code will call client.Close() and the await in the loop will throw an ObjectDisposedException. As far as I can tell, catching that specific Exception and breaking out of the loop is the neatest thing you can do.
Sunday, July 30, 2017
windbg SOS CLR
It's usually caused by some subtle environmental difference between the testing and live environments, such as a missing file or a bad configuration setting. Following are the steps I can never remember to help in this situation:
1. Get a copy of "Debugging Tools for Windows (x64)" (which was needed in my case) -- This is easier than it sounds, as there are many downloads available for a wide variety of platforms. I can't remember exactly how I found the right one for me, so proceed carefully.
2. Run windbg.exe and enter these commands:
.load SOS
sxe clr
3. Either launch the program to be debugged or attach to the process of a program that's already running. Start doing things in the program.
4. If the program throws an Exception the debugger will pause and you can issue this command to hopefully see a CLR stack trace to know where the problem is in managed code:
!clrstack
I won't waste space on the technical details here as this is just a reminder post. There are lots of online articles that explain what these windbg commands actually do. And there a lot more interesting commands and tricks to peek into the internals of what managed code is doing. Look for tutorials or cheat-sheets on this subject.
Monday, July 17, 2017
Windows 10 Open command window here
NOTE (Sep 2019) - Some recent Windows update has broken the following instructions. The registry change no longer returns the missing context menu. If anyone knows of a simple low-risk fresh workaround, please let me know.
Before the most recent major update to Windows 10 you could shift + right-click in a blank part of the Windows Explorer file list and get a context menu "Open command window here". I've been using that feature dozens of times daily for a decade. The menu was recently replaced with "Open PowerShell window here".
PowerShell is certainly an advanced scripting language, but it has irritatingly different behaviour for someone who just wants to run a few dir or pkzip commands quickly in the folder that is currently selected in Windows Explorer. Many old commands and /switch combinations are invalid and have to be prefixed with cmd /c to work in the PowerShell window, which wastes time.
There are many articles on how to get the old menu back, but a lot of them are dangerous or overkill. One article led me to the simplest fix. In a nutshell, login at the real Administrator, not an elevated user and go here:
HKEY_CLASSES_ROOT\Directory\Background\shell\cmd
- Let local Administrator take ownership of the key, subkeys and values away from TrustedInstaller.
- Give local Administrator Full Control of the key and subkeys.
- Rename the DWORD value HideBasedOnVelocityId to ShowBasedOnVelocityId.
These are rather strange steps, but they seem to be the least worst or dangerous that work. I worry that step 1 might have side-effects, but I'll ignore that possibility for now because of the productivity benefit of getting the old menu back.
Set Windows Service Description
This is a quick follow-up to the previous post about creating MSI installers for Windows Services.
The previous post describes how to add rows to two tables in an MSI database to promote the installer to formally manage the stop, start and deletion of registered services. Unfortunately, it's not possible to set the description of the service by editing the MSI database.
To change a service description (not the name or display name), you have to call into native functions in advapi32.dll which contains service controller functionality. You can find some online articles and samples of how to do this from managed C# code, but many of them are erroneous or questionable. I have determined the minimum correct C# code needed on Windows 10 to change a service description. The source file is a part of a utility DOS command in this DevOps repository:
https://dev.azure.com/orthogonal/MsiUpdater
NOTE: A follow-up experiment a few days later confirms that you can add a CA class to your setup project which calls the code above in the Install override to set the service description. Luckily, Visual Studio set the CA to run in the install sequence after the service processing.
Sunday, July 9, 2017
Visual Studio MSI Service Installer
Problem Overview
The "standard" Visual Studio way of creating a Windows Service when a product is installed is to create a Custom Action class derived from Installer which uses the ServiceInstaller and ServiceProcessInstaller classes to register the service. There are plenty of samples of this technique around.
The main problem with the "standard" way is that it only creates (registers) the service the first time you run the installer. The next install, say for an update, will crash because the service is already registered. I tried putting logic in the CA class to skip a duplicate install, but it there was a weird crash during install and it backed out.
Secondary problems with a "standard" service installer are related to the status of the service. There is no support to start a service after install or upgrade, or to stop it before uninstall or upgrade. You can write some OnAfterInstall event code to start the service, but any attempts to stop the service will be futile because the code runs at an inappropriate time in the Execute Install Sequence.
Once I realised that the Visual Studio and Framework class support for installing and creating Windows Service is seriously deficient, I had to find if there was a way of creating an MSI installer with full support for managing services. I have seen installers from Microsoft and other vendors that silently install or upgrade Windows Service with no manual intervention. So if other people can do it, then there must be a way I can do the same. The secret lies in two tables inside the installer.
Service Tables
An MSI installer file is a container for a large set of RDB-like tables which provide excruciatingly detailed control over install processing. Two of the tables named ServiceInstall and ServiceControl are specifically designed to control services. By carefully adding a row to each of these tables you can promote an installer to have the magical behaviour where a service is created, stopped, started and removed perfectly during install and uninstall processing.
The question becomes ... what's the easiest way of inserting rows into the service tables? If I could create some code to do this, then I planned to wrap the code in a library and a command line utility so that it can be called from MSBuild or Visual Studio project post-build events.
PowerShell
As an exercise in becoming more proficient in PowerShell coding I attempted to write a script that would insert rows into the service tables. If you run a search for "msi powershell opendatabase" you will many samples where people have used PowerShell to manipulate MSI files and their tables. There is no formal support for interaction with the installer COM object, so you have to use rather verbose and fragile reflection calls on COM objects.
After suffering terribly for many hours I concocted about 80 lines of script that were almost working. I could read the tables, but any attempt to open the database for update failed with cryptic and useless errors. At this point I threw in the towel, as I knew it would take hours of more suffering to perhaps solve the update failure. In any case, I felt like I was abusing PowerShell and using it for something it wasn't ideally designed for, as the script was becoming quite ugly.
On a whim, I opened up LINQPad to write some C# code to replace the script.
Managed Code
The C# dynamic type makes it really easy to work with COM objects. Within 15 minutes I had translated the PowerShell script into some C# code in LINQPad that successfully read and updated MSI installer service tables. The resulting code was a fraction of the original script size and it was much easier to read. To see how easy it is to read installer tables, here's some sample code the displays the contents of the [File] table.
string msifile = $"{testdata}\\MockService-1-0-4.msi"; Type t = Type.GetTypeFromProgID("WindowsInstaller.Installer"); dynamic installer = Activator.CreateInstance(t); dynamic database = installer.OpenDatabase(msifile, 0); dynamic view = database.OpenView("SELECT Component_, FileName, FileSize, Version FROM File"); view.Execute(); dynamic row = view.Fetch(); while (row != null) { string component = row.StringData[1]; string filename = row.StringData[2]; string filesize = row.StringData[3]; string version = row.StringData[4]; Console.WriteLine($"{component} • {filename} • {filesize} • {version}"); row = view.Fetch(); } view.Close(); //database.Commit(); // Only needed if you update something Marshal.FinalReleaseComObject(database); Marshal.FinalReleaseComObject(installer);
The code that scans and updates the MSI service tables has been formalised and placed into a DOS utility command. Source is in this DevOps repository:
https://dev.azure.com/orthogonal/MsiUpdater
There is a method in the file that takes all of the parameters required to register a service in an MSI installer's service tables. It throws if conditions are unexpected. I have wrapped a small console command around the method and I call it in the post-build event of Visual Studio setup projects to silently promote the MSI file into a formal service installer.
Summary
It took me many hours of suffering spread out over many weeks to find the MSI service specific tables, learn how to update them correctly, and to test the resulting installers have the correct behaviour on install, upgrade and uninstall.
I hope this post will help others avoid suffering.
Wednesday, July 5, 2017
log4net string pattern
Set the required values at runtime:
log4net.GlobalContext.Properties["mailHost"] = /*something*/
Use a corresponding property in the config file:
<smtpHost type="log4net.Util.PatternString" value="%property{mailHost}"/>
Sunday, May 28, 2017
String to multi-lines of max length
I found some of my old code from 14 years ago where I used a complicated loop to split strings, but now it was time to find a better way. I suspected Regex could be used, but I wasn't sure which syntax to use. After lots of tedious web searching I finally found someone who suggested something like the following, which I've tweaked slightly.
string s = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ..."; (cut) int maxlen = 60; MatchCollection mc = Regex.Matches(s, @"(.{1," + (maxlen - 1) + @"})(?:\s|$)"); var lines = mc.Cast<Match>().Select(m => m.Value); lines.Select(x => new { Len = x.Length, Line = x }).Dump();
The last line dumps the resulting split lines and their lengths in LINQPad like this:
Note though that if any words exceed the maximum split line length then they will be truncated.
Friday, May 19, 2017
Wiki Articles
Azure Table Storage as an RDB
A curious discussion of how I attempted to use Azure Table Storage like a relational database. It basically worked acceptably well by using combination of code generation, async coding conventions and a trick to generate auto-increment keys. However, as the code grew more complex I started to feel progressively more like I was abusing the technology and I eventually dropped the idea.
Database History and Design
Many years ago I had a popular page in my personal web site that described how I designed and normalized the SQL Server database tables for the Nancy Street collections database. The original page has been partly reproduced and modernised to describe the design of the most recently used database.Migrating away from SQL Server
In early 2018 I abandoned using SQL Server for my collections database. By migrating to Cosmos DB I underwent an epiphany about how many different database choices we have available now. Is your data best represented as simple tables, relational tables, documents, a hierarchy or a free-form node graph? There is something for you!
Sunday, April 9, 2017
Xamarin and Azure Storage
June 2022 - This article is probably irrelevant because the Xamarin development platform has changed significantly since this article was published. Portable Class Libraries (PCLs) are deprecated, Xamarin has changed and NuGet packages and dependencies have changed. This post remains here for historical interest.
There may be times when creating, designing and deploying a complete REST web service to support a mobile app might be overkill. Each service I create means more code, more contracts, more installers and more "stuff" to remember and manage.
As I've said in other posts, I'm a fan of Azure Storage tables, blobs and queues because they're so cheap, fast, easy to code, and best of all ... hosted by someone else. So I wondered if a Xamarin mobile app could directly use the Windows Azure Storage Nuget package, thereby using Azure Storage as a general purpose backend. Sadly, hours of experiments and web searching confirmed that a PCL (Portable Class Library) project cannot reference the package. I asked in the official Xamarin forums if there was any way a Xamarin Forms app could use Azure Storage, but after two weeks I never received a reply. With a fresh mind I ran more experiments and found the answer, and I have pasted the resulting forum post below as a reminder to myself and as possible help to others.
The WindowsAzure.Storage package cannot be directly referenced in a PCL project. I initially thought that there would be some workaround by adjusting the targets or build options, but the following statement on a Xamarin documentation page confirms I was wasting my time. The statement is strangely worded, especially the first sentence which misleads you into thinking that a Shared Project is mandatory, whereas it should say "the Azure library can be referenced from platform projects and a Shared Project is a convenient way of sharing code between the platform projects."
I found that you can add the WindowsAzure.Storage package to iOS and Droid projects and they work correctly. You can share the Azure code with the two platform projects using a Shared Project, which is what they were designed for. Then you can publish the Azure functionality to portable projects using the Dependency Service feature and an interface. I have never needed to use Shared Projects before, sticking only to PCLs because they are a neater separation of functionality. So now I can see my first real justification for the invention of Shared Projects, although you could also add the common source files to the project as links.
While discussing this issue in my local .NET forum it raised a bit of confusion between the packages WindowsAzure.Storage and Microsoft.Azure.Mobile.Client. The latter can be added to a PCL project, but it's designed to work specifically with Easy Tables, a web service and database schema you compose in a wizard style in the Azure portal. I've seen this done in a live demo and it was quick and easy and would be a great RAD choice if the situation arises. However I was not interested in Easy Tables, I wanted Azure Storage access using the general purpose package I've been most familiar with for a few years.
Wednesday, March 29, 2017
Xamarin Forms Observations
Early 2022 note: The Xamarin platform has improved a lot since this article was written. Overall stability has greatly improved, there is a richer set of controls and libraries, and the UI design experience can be regarded as acceptable. More importantly though, Xamarin has evolved into the MAUI platform, so this article will soon become a historical curiosity. I expect to make new posts about MAUI after I start using it later this year (2022).
The Xamarin development platform attempts to abstract and unify the development and distribution of apps for iOS, Android and Windows mobile devices. This is incredibly ambitious considering the vastly different technical details and cultural histories of the platforms. It's admirable of Xamarin to attempt to do this, but I must sadly report that they are only barely succeeding.
I have been using Xamarin Forms with Xamarin Studio on an iMac for almost two years, experimentally at first, and with intensity for the last several months to produce live apps. In that time I must also sadly report that Xamarin is one of the worst development platforms I have used in 30 years. It is unpredictable, unstable and riddled with quirks and 'gotchas'. Operating system updates or public library updates from Xamarin can break your development cycle utterly and hours (or days) of research may be required to get back-on-track. Debuggers will stall or not start, bizarre compile errors may appear, breakpoints stop in inaccessible places, intellisense produces nonsense, runtime app appearance is inconsistent on different platforms, and much more ... from the other end of the house my wife will often hear me shout the Xamarin mantra "everything f***ing doesn't work", which I will probably have engraved on my tombstone.
As a result of my suffering I have decided to document some observations and hints for other developers who may follow in my path of Xamarin development.
What I'm going to say assumes that you are writing "modest" sized apps with perhaps a handful of screens. By their nature, mobile apps should be easy to use and avoid deep navigation, otherwise users get confused and irritable. Apps should also not contain complex business logic, which should be pushed into the backend. So the following observations I'm making are relevant to apps of modest size and complexity. In cases where you have a complex app and a large team of experienced developers, then you can take my advice with a grain of salt. All of the apps I have written or seen in recent years have been of "modest" size.
Minimal Packages and Libraries
Don't use a Nuget package or library unless you really need it. There are libraries for busy controls, animated effects, platform specific bridges, dialogs, reactive events, IoC and MVVM patterns, service calls, and much more. Avoid them. The more "stuff" you put into an app, the more bad "stuff" happens. Many libraries do things can be done in standard code, but they obfuscate what's happening, complicate debugging and create "magical" code.
For example, I had an app that originally used a dozen various libraries and it would unpredictably crash on startup on certain Android devices and it would produce random crash reports with a stack trace deep inside a 3rd party library. After stripping out Acr.DeviceInfo, Acr.Userdialogs, Fody Weavers, FreshMVVM, FreshEssentials, Microsoft.Bcl, Refit, Rx (Reactive Extensions), SlideOverKit and Plugin.Connectivity, all of those problems vanished. Otherwise it could have taken weeks of effort to diagnose and solve the problems.
Lots of libraries also mean you might have complicated dependencies between them, which I found would make upgrading them a fragile process.
There are of course times when you are compelled to use a library, such as for charting, ink writing capture or exploiting device specific features. In this case, proceed by single steps, methodically, testing as you go, and keep praying!
Application Structure
Last week I was asking some highly experienced Xamarin developers what conventions or patterns they have found most popular (or best?) for the overall structure of a Xamarin Forms app. The answer was basically "whatever you or your team like". Which I found quite worrying.
I have seen a small Xamarin Forms app with 6 pages broken into 6 models, a service bus, IL weaving of binding properties, inter-controller messaging, Rx callbacks, etc. I haven't seen this sort of complexity since I worked on a huge Windows Forms program with 90 screens and dialogs about ten years ago.
I believe there is no need to over-engineer a small mobile app with complicated infrastructure, libraries and patterns. As a result of my recent experience, I have decided on a convention for how to structure a Xamarin Forms app that is simple and based upon classical MVVM. The app is basically two parts: the portable controller and the portable mobile project. Here's an overview.
Controller
The behaviour of the application is extracted into a single controller class in a portable library. The controller class implements INotifyPropertyChanged in the classical way. The controller class is effectively a state machine consisting of bindable properties and public methods. If there are large numbers of bindable properties, then you can use a T4 template to generate them into a partial class.
Mobile App
The mobile Application class sets an instance of the controller as the BindingContext in the App constructor just before the MainPage is set. The whole mobile app and all of the pages it contains are therefore automatically bound to the controller. The app binds its controls to the notify properties and calls the controller's methods to do work. As the controller does its work, it sets the properties and the app's UI responds. An IValueConverter class may be needed between some controls and properties.What I'm describing here is the simplest form of classical MVVM possible, where a UI is bound to a controller full of notify properties. I have found it to be simple and effective for writing modest sized Xamarin Forms apps. The controller class is decoupled from the UI and can be subjected to stand-alone unit testing. The controller can also be used to easily drive other types of programs such as Silverlight, WPF, ASP.NET, or even a console command.
Here's simple skeleton code of how I would code a login process. The login page binds Entry controls to the UserName and Password. Clicking a button calls the Login() method. After the method runs the controller changes state as properties are changed and the UI automatically responds.
public class AppController : INotifyPropertyChanged { public string UserName { ...notify property... } public string Password { ...notify property... } public Login Data { ...notify property... } public bool IsLoginBusy { ...notify property... } public Exception Error { ...notify property... } public async Task Login() { try { IsLoginBusy = true; Data = await service.RemoteLogin(_username, _password); Error = null; } catch (Exception ex) { Data = null; Error = ex; } finally { IsLoginBusy = false; } } }
It's important to think of the controller as a state machine. As Login() processes, the controller's properties change and finally indicate if it's now in the login-success or login-error state. The UI is designed to react to changes in state by binding. The visibility of a spinner could be bound to the IsLoginBusy property. If the login fails for example, an error label would become visible if the Error property is not null.
Some people try to abstract away the page navigation, but I have found this to be monstrous overkill. I believe navigation is a UI related task, so in the login sample, upon login-success I would simply put this single line of code in the code-behind.
Application.Current.MainPage = new FooPage();
Other Notes
- Don't make too many changes at once or you'll break something and go through hell trying to backtrack and find out what's broken.
- Don't try to target Windows except in the most trivial of app cases. This is especially true for tablet apps which look clumsy and non-standard when generated by Xamarin Forms. Write Windows tablet apps as UWP projects in Visual Studio and use the native controls like the CommandBar which give your app a natural feel on the platform.
- Accept updates for Xamarin Studio, Mono .NET, Xcode and other libraries with the greatest care. As I said earlier, updates can suddenly break your development.
- If you find breakpoints are not working, or stopping in weird locations, then close your dev apps, delete obj and bin folders from all projects and start again.
- You'll probably need the following special controls: (1) An iOS Entry control that doesn't perform spelling checks and capitalise (2) A bindable pickerNote-1 (3) A zoomable web view. There are samples of all of these available online, but be sure to use a simple one as many are over-engineered.
- Place copies of all shared images in one platform project only, then add links to them from the other project.
- I have never been able to get an Android emulator to work correctly. For several weeks I had a tablet emulator running VERY SLOWLY, then one day it stopped and never worked again (it just says "deploy failed"). I can only test by plugging a device into the iMac via a USB cable, and even then it's unreliable and I may have to take the cable in-and-out many times before it's recognised.
Service Calls
Your controller class will typically contact the "outside world" via web service calls, or perhaps to a local SQLite database (or Couchbase Lite which internally uses SQLite). In large applications many developers turn this level of code into an art form by the use of Dependency Injection (DI) to strictly decouple the service calls from the app. There is nothing to stop you doing this in a Xamarin Forms mobile app, in fact there are plenty of free frameworks available to encourage you, and there are lots of videos and tutorials on the subject.Continuing my claim that most mobile apps are of "modest" size, I think that using a DI framework is usually overkill. Just use common sense and put all service or database work behind an interface, then use some simple pattern like a service locator or even a static helper class to get implementation references.
You could, for example, use the Refit library to turn a REST API into an interface. And although I think this library is very clever and neat, I have previously received random crash reports from deep inside the library and have been unable to diagnose them. It was easier to remove Refit and replace the service calls with my own code, and the crashes vanished. I only had about 10 distinct service calls anyway, so after refactoring the code it was only marginally larger than the original, and it simplified debugging.
Friday, March 3, 2017
Registration of the app failed
Registration of the app failed.
Another user has already installed an unpackaged version of this app.
The current user cannot replace this with a packaged version.
The conflicting package is {NAME} and it was published by CN={GUID}. (0x80073cf9)
Normally you just click the Windows start button, type the name of the app to find its name and icon, then right-click Uninstall. That had no effect. I tried running as my normal user and Administrator but it had no effect.
Run Powershell ISE with administrative rights. Run Get-AppxPackage -all and you should see the offending package near the end of the long list (you can use filter arguments or pipe the output into a filter, but I forget the syntax). Look for the PackageFullName and pass it into this command:
Remove-AppxPackage -Package PackageFullName
After doing that I could run and debug in Visual Studio again.
Saturday, February 4, 2017
TaskDialog (Windows API Code Pack)
August 2024 Note: The following old article is well out-of-date now. From .NET 5+ you can use the TaskDialog class which is
provided in the System.Windows.Forms
namespace and library, and it exposes more options than the other free libraries. For more
information see: TaskDialog,
TaskDialogPage and
TaskDialogButton.
Even using the most basic options produces pleasant looking modal windows that are good replacements for the standard MessageBox. Here is a simple example with some hyperlinks and a See details expander.
Several years ago I used the TaskDialog class in some desktop apps as a nice replacement for the simple MessageBox class. Following is a sample of a slightly advanced TaskDialog where you can expand extra information in the footer. You can also use "command links" instead of the standard buttons, add custom controls like check boxes and progress bars, or expand the middle of the dialog instead of the footer.
If I remember correctly, the TaskDialog class was part of the Windows API Code Pack for Microsoft .NET Framework in the MSDN code gallery, but all links now seem to be dead and it looks like the API Code Pack is no longer supported.
Luckily, web searches eventually located some copies of the API Code Pack that had been archived, and surprisingly, the whole lot downloaded and compiled cleanly. I considered extracting the TaskDialog related classes out of the large quantity of complicated Interop code, but then I discovered someone had simply turned the code into a set of NuGet packages. So it's easiest just to use the NuGet package and ignore the extra classes it contains. This is the package to reference:
Microsoft.WindowsAPICodePack.Core
The package library contains the TaskDialog class as well as dozens of other classes which use Interop as bridge to Win32 functions related to networking, power management and the Shell.
For my purposes I only needed a specific subset of the full functionality of the TaskDialog class. I wanted to use the expandable footer with links, and to use "command links" instead of the standard buttons.
Beware of the following:
- You need an App.manifest file entry to force your application to use a modern version of the Windows common control library. See the file in the sample project.
- You have to set the dialog's Icon and InstructionText properties in the Opened event, otherwise the dialog may be incorrectly sized. This is an irritating quirk or bug that took a bit of web searching to solve.
- Clicking a "command link" doesn't close the dialog, you have to call Close in the link's Click handler.