Monday, March 29, 2021

Here, I made a way to navigate between ANY files in Visual Studio

Being able to navigate between different parts of code is really useful. You simply hold down the ctrl key and click on an item, and you're taken to the definition of a method, or class, or whatever. Or, maybe you use a shortcut or context-menu entry to do the same. However you do it, it makes navigating a codebase much easier and saves untold time.

But what if you want to navigate between files that aren't all code, or in the same programming language?

Visual Studio doesn't support this, but don't worry, I can help. 

Introducing CommentLinks, a Visual Studio extension to enable links between any files from within the Visual Studio code editor.

the extension logo
The idea is simple. In your code, you put "link:" followed by a filename, and then a button (see below) is added that, when clicked, will take you to that file.

button shown next to the text "link:MapManager.js"
It will search for files anywhere in the solution. The file doesn't have to be in the same directory, project, or in any project. It just needs to be visible in the Solution Explorer.
If there are multiple files in the solution with the same name, you can specify the one to find by including the directory name(s) to be clearly unique. e.g.:

link:otherjs/device.js (either directory separator)

Want more than simply opening a file?

You can open the file at a specific line by adding "#Lnn" (where nn is the line number) after the file name. e.g.:


You can open a file and search for specific text by including it after a colon (:) immediately after the filename. Or after "#:~:text=" to match the convention for text fragment anchors.


And, it can handle spaces in the search text or filename by using percent-encoding (so a space character is replaced with "%20") or by putting them in quotes (single or double.) e.g.:

link:device.js:"getInfo = function ("
link:"device.js:getInfo = function ("
link:"name with spaces.js"

You can navigate within the same file (to search for subsequent occurrences of text) by specifying the file's name and then the search text.

You can open any file on disk by specifying the full path. e.g.:


You can even use it to run arbitrary commands by adding "run>" after "link:" This can be useful to open other applications, open files in their default application, or to run commands with custom schemes/protocols. e.g.:

link:run>mockup.pdf  (assuming in the same directory)

Don't want to use "link:" before the file name?
That's ok. You can change this to any text you want by going to Tools > Options > CommentLinks and changing the Trigger word.
partial screenshot of options dialog showing the trigger word setting

The name might be misleading. It doesn't just work with comments but anywhere in the code file. The name came from the original intention and the fact it was expected only to be used in comments. It works anywhere.

Get it from the marketplace and let me know what you think.

Want to see it in action? See this quick video I made previously showing some of the features:

This is part of a collection of Visual Studio extensions I have made. You can see them all at 

I originally created this at the suggestion of one of my GitHub sponsors. Not only am I really grateful to the people who sponsor me (the current ones and those who have sponsored me for a period of time in the past, but I also try and do things to help them.

I can't always build everything my sponsors want or need, but I'm inclined to help them more. 😉

This was built for a specific niche use-case, but other people have already found this extension and requested many of the features it has. It turns out that working with multiple file types in VS isn't something that everyone needs to do, but the people who have to do this are very disappointed with what comes in the box and are keen to see additional capabilities added.

One such person who wanted a new capability even paid for this to be added. They wanted a feature I wasn't sure about adding. I was going to suggest to them that this was the sort of thing I'd be more inclined to do for a sponsor, but before I could email them, they'd sent me some money. Not a lot but not a trivial amount. That feature was added quickly ;)

I'm still not entirely sure how I feel about someone giving me some money and then asking for a feature. Still, I didn't want to discourage someone enthusiastic about the project. I especially didn't want to discourage anyone willing to give money to an open-source project.

Based on the time required to implement and support the feature, it's less than I would charge for regular work at an hourly rate, but it was certainly appreciated.

Wednesday, March 10, 2021

When everyone's working together for the collective good

"Why do you care? You're leaving at the end of the week."

I've been reading, the excellent, Turn the Ship Around by L. David Marquet.

Cover image of the book: Turn the Ship Around (by L. David Marquet)

It's made me reflect on two work-related scenarios:

The first is where the above quote comes from. It was after the weekly developer-team meeting and two days before I left the company. In the meeting, I advocated for a change to a process that I thought would improve the time and effort required. I wouldn't be around to see the benefit, but everyone else would. That a colleague asked me why I cared when I'd be leaving confirmed that I wasn't a good fit for the company I was soon to leave. I wonder how many people in this world are like this colleague. I hope it's only a few. I fear it's a majority.

The second scenario is more current. I'm being passively-aggressively encouraged to step away from an open-source project I've been contributing to for several years. A change of leadership and priorities (that are no longer public) clash with my desire to understand why decisions are being made and being open about what is happening in an open-source project. Not wanting to stay where I'm not wanted, I'm thinking about where to best focus my time and attention next. But I'm also concerned about what I'm leaving behind. Before I go, I want to ensure I'm leaving it in the best possible state, and that means continuing to strive for improvements. I'm still trying to work out how to do this in the best possible way and am open to suggestions.

I want to make things better. In addition to the products I work on, I desire to make the places I work, the projects I work on, and the processes I use to make things better.

Is this misguided? I hope not.

Is this all a case of me caring too much? If so, how much is too much?

What's it like when everyone on the team is working to make not just a great product but improve the process as well? - If you have experience of this, I'd love to hear about it.

Is your organization looking for an experienced .NET & Windows developer who isn't afraid to speak up and improve processes?  I might be a good fit. I'd love to hear from you too. 😉

Sunday, March 07, 2021

Open-source is like politics - show up to make a difference

Open source is like politics.
But, probably not in the way you're thinking. Not politics in a negative sense as if it's all about power, domination, and control.

hand putting voting slip in box
Photo by Element5 Digital on Unsplash

I think open source is like politics in that many people think about it in the same way as they do the politics of local government.

What do these refer to?

  • Generally, people think it's a good idea.
  • Typically, people take what they can from it and don't give back beyond what's required/forced.
  • People think about getting involved a lot more than they do.
  • Lots of people have [strong] opinions but don't act on them.
  • A few people contribute a lot (of time and effort).
  • People who contribute are rarely rewarded or compensated well.
  • When a person is directly affected, they may show up wanting action on the issue that affects them. But, their desire to look at (or consider) anything else is small, and their enthusiasm is short-lived.

Were those comments about local government or open-source software?

I hope you can see they apply to both.

I think the similarity also extends to how to make a difference in each.

Who are the people who make a difference in local politics?

They're the people who turn up. Repeatedly, regularly, and when it's not always in their immediate interest. They do things to help the community.

Four things to do:

  1. Show commitment. Turn up regularly. (Not just to be seen but also because it's often the only way of knowing what's going on.)
  2. Work for everyone's benefit. Do things that help others, not just you.
  3. Share your knowledge. Not to show off but to help others.
  4. Think long term. Not only immediate benefits.

Doing the above will build trust and a reputation that will support your big ideas down the line.

Yes, large organizations and people with a lot of money, influence, or celebrity can come in and make a big difference in a short amount of time. I'm assuming you're not one of them (I'm not either), and so you have to do what you can with what you've got.

Does this really work?

There are some examples from WinUI where community members (people) have suggested new controls and an alternate syntax that were then implemented by the team.

But what about something bigger?

The most prominent example I have of influencing a Windows-related open-source project is adding support for Visual Basic(!) in Windows Template Studio. This was a big deal as it was a significant change and would be something with long-term consequences.

This is something that I (as a non-Microsoft employee) persuaded Microsoft to add.

  • I did this for a project in which I had built a reputation for regularly participating.
  • I'd previously displayed knowledge so that the project leads valued my opinion.
  • I backed up my suggestion with data. (Not data I had but data I knew they could get access to and assumed would support my proposal.)
  • I suggested something that aligned with the larger goals of the project and organization.
  • I suggested something that wasn't in my immediate interests but would help underserved members of the community.
  • I showed how I could help make the change easy by volunteering to help with the work.

Only big changes?

Let me be clear, contributing to open-source isn't only about big changes!

Every little helps! It's not just true of supermarkets. All the small things we do add up. 

One of the great benefits and advantages of open-source software is that it can be built on many people's wisdom, knowledge, skills, and contributions. Not everyone can make large contributions (in size or number) to every project. But, all contributions help, regardless of size, when they work to build better software for everyone.

Improving documentation, giving feedback on a spec or proposed feature, adding a real-world example, and sharing details of a project. These can all be just as valuable as writing code.  

There are projects where I've only ever made small changes. Yes, even only fixing typos. I know these don't make a massive difference but they do help those who come after me and they do make a difference.

Start small and then work up. Or don't. Staying small is good too.

I drafted the first version of this post in early February.

As part of the WinUI Community Call on 17th February  2021, they included a section on "Maximizing Your Influence on WinUI." (Watch that section on YouTube)

There was lots of good advice in that call/video, and I think it complements what I've said above.

If you don't want to watch a 15-minute video, here are some screen grabs of the slides shown, or check out this document where they summarize the key points.

Phrasing Postively:- Emphasise- Constructive action (what is "won" by doing this?); Clear & detailed "why" - assume an explanation is needed.-Main benefit? -Current alternative? -comparisons can help, but shouldn't replace detail; Mutual empathy

Phrasing Positively | Before: The state of platform is disappointing. I am not going to consider WinUI until my trust has been earned.  After: Deprecation of past frameworks has left me burned. The following would go a long way in earning my trust because my company is trying to launch an app next year and will need it supported for at least 5 more: - OSS, - High confidence 5-year roadmap, - Guaranteed support timeline

Phrasing Positively | Real impact: Team's spec writing process - a story about customer feedback

Feedback shapes the product | Example areas: - Features you care about for your own real apps & projects - Ways you'd like to see the team improve community engagement & interaction - Stories of how our roadmap plans do (or don't) enable your apps - Ways we can improve the developer experience (tooling, docs, blogs, etc.) - Bugs & issues in WinUI!

Feedback that shapes the product | Less likely to shape the product: - Feedback on products beyond WinUI (e.g. Start Menu) - "Stop doing X, start doing Y" - Reaching beyond Windows | Guidance coming soon!

WinUI isn't a project where you can easily come in and make big changes, or expect dramatic changes in direction. But you can help make a difference, not only to the project but also to the lives of the developers and the people who use the software they create.

My impact on the spec process is the most significant change I've made there, but I'm not disheartened. I know I've made a difference that helps others and I hope that my contributions, suggestions, feedback, and support will continue to have a broader impact in the future.

Keep showing up.

Let's make a difference together.

Thursday, March 04, 2021

It's sad and happy to deprecate something that's no longer needed.

A couple of years ago, I built a tool called UWP Design-Time Data (GitHub | NuGet)

It attempted to provide some functionality that was available in Xamarin.Forms (XF) but not in UWP.
I saw someone from Microsoft show the functionality available to XF developers and immediately wondered two things? When will that be available to UWP? And, how can I recreate that until then? 

partial screenshot of the Visual Studio XAML editor and designer showing the above described functionality

Based on the limited XAML extensibility of Visual Studio, I did the best I could. It wasn't perfect, but it was better than nothing.

Mine was never meant to be a permanent solution.
I always expected and hoped for it to go away. The last time I gave a conference talk about XAML tooling (in Feb 2020), I even talked about mine being a temporary solution.

Version 16.7 of Visual Studio (actually released in the middle of last year) added this as part of several other XAML-related improvements.

Today I finally got round to updating the GitHub repo and Nuget listings.

I'm not going to take them down, but they're essentially deprecated, and I don't ever expect to update them again.

It's sad to think that this is the end of this project.
All the time and effort I spent on this has no future value.
Hopefully, at least some of what I learned in the creation and support of this will be useful in the future.
But, it's good to know that this small but useful functionality is now available to all VS users, not just those who hear about and install my extension.
It's also good that to think that I was able to help some people for a while.

Today is a time to celebrate what was and look again at what comes next. 🥂

Never just "should"!

 TLDR: never use "should" without explaining why.

It should never have come to this
Photo by Brett Sayles on Pexels

How many times today have you used the word "should"?

"You should do X."
"They should do Y."
"It shouldn't be like that."

Or any variation on that theme?

"should just" is even worse.

"You should just do X."

This use of "just" implies it's simple or easy to do. It implies little effort will be needed. But, that isn't always the case.
What may be simple for you may not be for someone else. 
What may seem simple from the outside belies what may actually be required to make it happen.

Avoid saying "just" when it implies simplicity unless it's something you're doing and already know how to do.

"Should" is more complicated.

You may be familiar with the MoSCoW method of prioritization. I was taught this at university and have never liked how vague and open to interpretation the categories still are. I've been in far too many meetings throughout my career that have come down to different understandings of what needs to be delivered. In these situations, "should" is either considered to mean:
  • It would be nice if this was included, but it isn't strictly necessary. 
  • It's used to indicate or identify something that can be deferred until another time.
  • Or it's understood to mean something that can only be left out without a VERY good reason. 
"Should" can cause confusion.

Or, maybe you're used to seeing SHOULD in documents where it takes the definition from RFC2119.


This word, or the adjective "RECOMMENDED", mean that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course.

This is the crux of my issue. It admits that there are one or more reasons why something should or shouldn't be done.

If you say that "something should be done," why should it be done?
Because of your personal preference?
Because of a contractual reason?
Because of knock-on effects and dependencies or consequences elsewhere?

Without knowing the reason(s) that something "should" be done, we're forced to guess.

If the person saying "we should do X" has the ultimate authority to make decisions, then knowing why has less importance. It might be helpful, useful, or empowering to understand why, but it's not essential. 
In this scenario, a different word is more appropriate. "Please do X." is more specific and more helpful. Even if it's a bit abrupt, it's better than adding unnecessary and potentially confusing words.

If not direct instruction, knowing why is essential. If an opinion, we may be free to ignore it. If a contractual requirement, then we need to know to make sure it happens. If a pressing priority, time-sensitive or will have considerable consequences if not know and acted upon, then we need to know.

It might not be as severe as "you should stop that small child walking into traffic," but explaining why something "should" be done is considerate.

"You should do X" is rude.
"If you do X, it will help me..." or "if X is done first, it will unblock this other work" or "we need to make sure that we do X" are respectful and collaborative. 

Words matter.

Monday, March 01, 2021

You must be insane to be a software developer

"The definition of insanity is doing the same thing over and over again and expecting a different result." - Probably not Albert Einstein.
Albert Einstein - pixabay

When debugging, I repeat myself frequently. Doing the same thing over and over again and hoping for a different result.

There are times I wish I had alternatives, but I know no other way, and so I rerun the code. Each time hoping that something will be different.

Rarely it is.

But it's those rare occasions that make this approach acceptable, if not necessary.

Note. I'm not talking about Heisenbugs. They're a separate, additional thing.

Sometimes repetition is the only option. An issue is observed or reported as only happening "some of the time." In such scenarios, the only available option is to run the code or perform an action to see when the bad thing occurs. Then try and identify what the difference was when it happened. 

This is important because, without consistent reproducibility of an issue, it's impossible to be 100% confident of an applied fix.

I know many developers and organizations who dismiss reports of occasional issues. If the problem doesn't happen all the time, so their thinking goes, it must not be with their software. Or they assume that the responsibility to fully identify the cause of the bug is not theirs. Or, they think their time would be wasted trying to reproduce such bugs consistently.

This comes down to perspective and priority.

Is the developer tasked only with writing the code? Or is the code a means to an end?  

I think the goal of writing code is to deliver value. I don't see any value being delivered by dismissing a bug report because it's not consistently reproducible. Instead, I see this as a challenge.

I assume that the person reporting the problem has given all the information they can. Now the challenge is for me to investigate the situation and find the solution. I'm a detective. 🔍

Being a bug detective might (and often does) mean repeating tasks to see when there is an inconsistent response and then trying to work out the cause. Such problems usually come down to one of four things.

  • It's time (or date) dependent.
  • It's machine/OS/configuration dependent.
  • It's a race condition.
  • It's dependent on the number of times the function is executed.

That time it failed, and the time before it worked even though the code hasn't changed. What else has changed? And how can I test that?

Note. I know there are obscure edge case bugs that it's not the best use of anybody's time to address. Let's assume that here I'm talking about bugs that the business deems worth spending time/money/effort fixing.

I wonder if there are better ways to investigate such bugs.

I wonder if there are tools to help with this. (I've found references to a few things that no longer exist but nothing current. 😢)

I will frequently run unaltered code and hope for a different result. Surely I'm not the only one...

Sunday, February 28, 2021

Tried everything? - Methodical debugging

I've been thinking about "methodical debugging" and would love to hear your thoughts and experiences.

Early in my career, I worked in a head office IT department. While the main focus of my role was software development (mobile, desktop, and web applications), I was also the last line of support for any problems.

If a customer had a problem, they'd contact the branch. If no-one at the branch could help (some branches had their own IT staff), it would be escalated to the helpdesk. If the helpdesk could solve the problem, they'd contact me.

When contacted by the helpdesk about an issue, I couldn't immediately answer, I'd ask, "what have you tried?" The answer to this was always "everything!"

This bugged me for two reasons.

  1. It definitely wasn't true because if they'd tried everything, they'd have found the answer. Or if there was no answer, they'd know that too.
  2. It meant they didn't have a list of what they'd tried, so I'd have to go right back to the beginning, assume nothing, work through all the possible issues, and find the resolution that way. This duplication of effort was inefficient and felt like a big waste of time.

In retrospect, I wish I'd taken the opportunity to define a list of questions that could be asked and things to document when they're asked. 

But I don't have a time-machine and can only think about improving things in the future.

And think about the future I have. Specifically about debugging.

My debugging has a tendency to be a bit haphazard. I'll skip from trying one possible idea to another until something works. I'll do my best to try and only change one thing at a time, but I can't be certain that's always the case. Especially if the issue isn't quickly resolved, I can easily forget all the things I've tried and suspect I may end up trying things more than once.

While this approach gets me to an answer, I'm not sure it is efficient, effective, or the best way to learn the most about what doesn't work or why.

There isn't a lot (I can find) written on the subject, and the little there is typically starts by acknowledging the lack of writing in this area.

This feels like an area that, if better understood, could greatly help many people in the industry.

What about you, do you do anything other than what I've described above?

If you document things as you debug, I'd love to know more about the process or technique you use and how it's helped you.

Aside: The role I mentioned at the top is where I developed the attitude that I must find the solution if there's a problem because there isn't anyone else to ask. This was before YouTube and StackOverflow. When I got truly stuck, there were only web-based bulletin boards and forums to ask for help, and these rarely proved fruitful. Even now, when I hit a problem, I usually stop and try and work it out myself before searching for error messages or the like. I find this approach valuable as it forces me to think more widely about the problem and explore ways to solve it. I've become a better developer by taking an attitude of "I don't know how to solve this yet(!), and in learning how to solve this, I will gain knowledge that helps me in other ways too." This is in contrast to an attitude I often see in others, which is to get the answer to the specific problem they are currently facing as quickly as possible and then move on.

Saturday, February 13, 2021

100,000+ "thank-you"s

At some point in the last couple of days, I passed a milestone. 

The public* packages I have made available on NuGet have now been downloaded over one hundred thousand times.

28 Packages | 101,283 Total downloads of packages

In the grand scheme of things, this isn't a large number. There are many packages with hundreds of thousands of downloads. Some of those packages get more downloads on an average day than my packages have had over several years.

So does this seem like a milestone even worth acknowledging?

I think so.

The above figure represents thousands of people (developers--they are people too) who I've been able to help save time and effort. It may be with a trivial, one-off task, or it may be something they use multiple times a day and in apps used by thousands of people. 

Download numbers like this are only relevant to the packages and the people who make them. I've known some developers over the moon because they never expected even a few hundred people to use the thing they created. In contrast, I've known other developers complain that the thing they created didn't get millions of downloads in the first few days.

This is a bigger number than I ever expected to reach but I also expect to produce many more packages in the future. Little by little, as I stick around and keep doing the work, the numbers go up. 

These tools may not have made a dent in the universe but they probably contribute to a small dimple. Maybe, over time, more dimples will add up to a dent. 

They may not be the most popular packages in the world. But that's not what I set out to create. I saw gaps where tools would be useful and produced tools to help with those situations. A small gap in the market that one person can quickly fill will rarely become massively popular, so I had set my expectations accordingly.

However, a niche product can be really useful to people who need it.

Many of these packages are now redundant (because they relate to platforms that no longer exist--the platforms aren't publicly available or supported in any way) but these packages serve as a reminder to myself (and maybe others) that I'm here for the long term. I haven't just shown up in the open-source world, made something quickly, and then gone away. It's also a reminder that small gaps can also be worth filling. Not only do they help with the things that I work on but they are also useful and helpful to others too.

I think the above figures help quantify part of my contribution to making the world a better place through open-source software.

Of course, my contributions aren't just about niche tools for underserved platforms and environments. I also contribute to much larger projects too. Some with many millions of downloads.
The combination of working on a breadth of smaller scenarios with my tools and much larger projects as part of a group of contributors, helps me become a better developer.

* There are a couple of private ones I have removed ("unlisted") because of name and dependency changes. They definitely shouldn't be used.

Tuesday, February 02, 2021

Here, I made this to search StackOverflow for your error description

Another small update to an existing Visual Studio extension.

ErrorHelper allows you to work more easily with the descriptions on the Error List.

Context menu showing options for: Copy Description, Search Description, and Open Url
You can copy the description (not just the whole line--as is the default behavior).
If the description contains a URL you can open that directly.
And you can search the internet for the description (error) message.

The alternatives to using this extension is to manually type the error message or URL. Or you can copy the whole line (including all other columns) paste it somewhere and delete the bits you don't want, or copy (again) the bit that matters and paste that into a search box somewhere else instead.

I find right-clicking and then clicking on "Search Description" much faster and easier!

In today's update (v1.7), I added the ability to set the search engine as StackOverflow.

Configuration /Options window showing search options of: Bing, Google, and StackOverflow

Yes, searching in Bing or Google quite often takes you to StackOverflow. If you find that's the case for you, this option now cuts out the middle-man.

This improvement was from a suggestion I received on Twitter. If you have any other ideas for making this (or any other extension better) then feel free to tweet me a suggestion too. 

Here, I made a thing to save time building all projects in a solution

Compilers are complicated. They don't always behave the way you'd expect.

For instance, if you have a resource that has the Copy to Output Directory setting of Copy Always this can force a whole compilation even if nothing has changed!

I've never needed this but sometimes this is the default setting for some file types. As this can lead to me waiting while projects are being unnecessarily recompiled I previously created a Visual Studio extension to detect this and warn me so I can correct offending entries and save myself time.

"Copy Always" with a line through it

Today I've updated it so that it can be used from the context menu of the Solution entry in Visual Studio's Solution Explorer.

Just a little thing but I hope it will help at least one person.


Thursday, January 07, 2021

Fallback Fonts in XAML

I have to admit that I feel foolish for not knowing about this before. Let me tell you about the power of fallback fonts in XAML so you can avoid the ignorance I lived in for so long. I'm doing this in UWP code but the same applies to WPF.

Let's say you want the following displayed (with XAML) as simply as possible.

I [filled heart] You [smiley face] [red lips] [music notes]

The "I" and the "You" are rendered with the "Segoe Script" font.

The "filled heart", "smiley face", and "music notes" are from the "Segoe MDL2 Assets" font.

The "red lips" are the "kiss mark" emoji.

Off the top of your head, how do you render this?

You might put multiple TextBlocks next to each other (probably in a Horizontally-oriented StackPanel) and assign different FontFamily values to each one.

Or, you might use multiple Runs within a single TextBlock and assign different FontFamily values to each one.

But, let me tell you there's a way to do this with a single TextBlock and no runs!

The XAML looks like this:

<TextBlock>I &#xEB52; You &#xED54; 💋 &#xE189;</TextBlock>

To get the script font I set the FontFamily to "Segoe Script" and I get this:

I [unrendered character] You [unrendered character] [red lips] [unrendered character]
Note that the red lips / kiss mark are rendered as a glyph even though they are not in the "Segoe Script" font. This is due to a built-in feature of XAML text rendering. The underlying text rendering system is doing a lot of work to try and avoid try and display all the characters. When the specified character isn't in the defined font it tries to use a value from the `ContentControlThemeFontFamily` resource. This is a list of fonts that it looks in for something to display the appropriate glyph. By default, this includes "Segoe UI" and "Segoe UI Emoji". It's the second of these that means that emojis are rendered in most places without any extra effort needed.

So, we're part of the way there by specifying a single FontFamily.

FontFamily="Segoe Script"

But what about the glyphs from the "Segoe MDL2 Assets" font?
Well, just as in CSS it's possible to specify multiple fonts/typefaces to use as a fallback, the same is possible with XAML.
Why I never thought of this before I don't know.

FontFamily="Segoe Script,Segoe MDL2 Assets"

In my head, I always thought of the HTML/CSS specification of multiple fonts as applying all or nothing to the text. If the first specified font is available use that for all the text, if it's not available use the next font, etc.

I don't know if HTML/CSS rendering is cleverer than that (please comment below, if you know) but the XAML text renderer is smarter than just using the same font for everything. It uses the specified list (family) of fonts on a character by character basis.

If it can't render a character in the first defined font, it will look to the next in the family and if none provide the character it will look to the ContentControlThemeFontFamily resource. Only if none of these fonts have a glyph for the character do you get the placeholder for a missing glyph.

This is a simplified explanation as the real solution considered different languages too. Don't ever think displaying text is simple. ;)

I discovered all this after going down a rabbit hole investigating how to avoid characters not displaying. I started by looking at intercepting what is displayed on the screen and then checking all the characters there are in the font family specified. (I didn't even consider multiple font-family values.) That lead me to investigate how to detect binding changes which led me down a side path looking at how the `x:bind` markup extension creates and reflects binding changes.

I wanted to know when any text in an app isn't rendered correctly so that the app could be updated to support the fonts/glyphs it needed so users can always see the characters they expect.

Now knowing about fallback fonts, I think a simpler solution is to make sure I include multiple fonts in the default FontFamily used within apps.