Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
157908 stories
·
33 followers

Database Performance Health Check: Six Months Slow, Fixed in 75 Minutes

1 Share

A gentleman joined my call looking like he had not slept in a month. His website checkout had been slow for six months. Six months! He threw money at it. A bigger server. More memory. He even rewrote the same code twice. The slowness said thank you and stayed. Then, about seventy five minutes into our database performance health check, we found the troublemaker sitting right there on the screen. By the time our tea went cold, the fix was already live. So please, do not call me a genius. I just looked in the right place. That is the whole trick.

Database Performance Health Check: Six Months Slow, Fixed in 75 Minutes hero-800x450

Six Months of Guessing

The team was not foolish. They were just tired and scared.

The server was fragile. Nobody wanted to poke the bear. So every fix was half a fix. Add a little CPU. Add a little RAM. Cross fingers and pray.

You could see it wearing on everyone. The support inbox kept filling up. The boss asked about it every single morning, in that gentle voice that is somehow worse than shouting. One developer started calling the checkout page the haunted house. Everybody had a theory. Nobody had a measurement. And a theory without a measurement is just a rumor wearing a lab coat.

But nobody asked the server the simple question. Servers are like people. They tell you their problem, if you ask and listen. Nobody had checked the wait stats, which is just a fancy way of asking what the server is standing in line for.

Six months later they had a bigger, costlier server. And the same headache. Just with a nicer view.

What Those Seventy Five Minutes Looked Like

We did not touch the code. We let the server talk. And it talked. Loudly.

So we lined the suspects up and let the numbers do the talking. No opinions allowed. The server does not care what you believe. It only reports what actually happened, and it keeps very honest books.

It was not short on power. It was stuck in traffic. All that shiny new hardware was solving a problem that never existed. Classic.

Then we found the guilty query. It was not ugly or clever. But the server had memorized one bad way of running it. Once. For one giant customer. Then it used that same plan for everybody else. Even the fellow buying one toothbrush.

Imagine cooking a full Thanksgiving dinner for 500 people every time one person rings the doorbell. That was our query. Every single time.

On top of that, the server was reading an old map. Stale statistics, we call it. The data had quietly moved house, and nobody updated the address.

No fancy tools needed. Just four boring questions. What is the server waiting for. What is it running the most. When did the data change shape. And does the old plan still make sense.

Ninety minutes of chasing ghosts became seventy five minutes of following footprints.

Please Do Not Call Me a Genius

There was no magic on that call. Just a checklist and a little patience. Any good DBA would have reached the same door. I just knew which one to knock on.

People expect a magician. They want the cape, the smoke, the big dramatic reveal. But real fixing is boring on purpose. You rule things out one by one, until only the truth is left standing, looking a little embarrassed.

The difference was not brains. It was four hours of doing nothing but looking. Six months of part time guessing will lose to ninety minutes of full time looking. That is not talent. That is just math. And math never takes a day off.

What a Proper Health Check Does Differently

This is exactly why I treat a database performance health check like a serious thing. Not something you squeeze in between two meetings and a lunch that is going cold.

  • A clock keeps everyone honest. Four hours, maximum. You chase the loudest problem first, not the most interesting one.
  • Look first. Touch later. Nothing changes until we are sure. “Let us just restart it and see” is how every six month story is born.
  • You keep your password. I never ask for it. Never. You drive. You see every command before it runs. No black box.
  • Every suggestion has three doors. Do it now. Study it later. Or skip it happily. Your house, your rules. I am only the guest who noticed the leaky tap.

If Any of This Sounds a Bit Too Familiar

Maybe you have your own little six month story. A problem that gets a fresh bandage every few weeks. But somehow never heals.

Here is the good news. It is almost never a deep mystery. It is usually just something nobody has looked at yet. Nobody sat down long enough to spot the pattern waving at them.

Most teams wait far too long to ask for help. Not because they are lazy, but because asking feels like admitting defeat. It is not. The bravest thing you can do with a slow system is to stop guessing out loud and let someone measure quietly.

That is the whole idea behind the Comprehensive Database Performance Health Check. Four focused hours. No access to your server. Every script goes home with your team, so you never need me again. And a fixed price, agreed before we begin. No surprises hiding in the bill.

Most of the time, things start moving before we even finish. Often in the first seventy five minutes.

Six months is a long time to keep guessing, my friend. Your users are not that patient. So stop guessing and start measuring. With someone who has done this about four hundred times. Your database is not angry with you. It is just waiting to be understood.

Guessing was never cheaper than measuring. It just sends the bill later, and it always adds interest.

Reference: Pinal Dave (https://blog.sqlauthority.com/), X

First appeared on Database Performance Health Check: Six Months Slow, Fixed in 75 Minutes

Read the whole story
alvinashcraft
6 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Anthropic’s landmark $1.5B copyright settlement is approved

1 Share
The final approval settles one case, but it doesn't resolve the broader issue of using copyrighted works to train AI models.
Read the whole story
alvinashcraft
50 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Copying HTML Tables as Text, Markdown, and CSV

1 Share

Earlier today and in a non web-related conversation, I saw someone talk about copying tabular data into different formats, like plain text and CSV. It got me thinking - this could be a useful feature for web sites and surely something pretty trivial to do in JavaScript. So I whipped up a quick demo.

First, I added a table of course:

<table id="table1">
	<thead>
		<tr>
			<th>Name</th>
			<th>DOB</th>
			<th>Gender</th>
			<th>Breed</th>
		</tr>
	</thead>
	<tbody>
	<tr><td>Whiskers</td><td>2019-03-14</td><td>Male</td><td>Maine Coon</td></tr>
	<tr><td>Luna</td><td>2020-07-22</td><td>Female</td><td>Siamese</td></tr>
	<!-- bunch more rows -->
	<tr><td>Nala</td><td>2022-04-18</td><td>Female</td><td>Persian</td></tr>
	<tr><td>Molly</td><td>2022-01-11</td><td>Female</td><td>LaPerm</td></tr>				
	</tbody>
</table>

Alright, so to enable this feature, I thought data attributes might be nice. This would let you use it on anything, a button, a link, image, etc. The data attribute would be responsible for pointing to the table (via an id) and specifying a format. Here's an example:

<!-- data-copy defaults to text -->
<a href="" data-table="table1">Copy as Text</a> ~ 
<a href="" data-table="table1" data-copy="md">Copy as Markdown</a> ~
<a href="" data-table="table1" data-copy="csv">Copy as CSV</a>

Now all I need to do is wire it up to code that looks for data-table and adds the appropriate handlers. The first part is trivial:

const links = document.querySelectorAll('*[data-table]');

Note that while I named the variable links, you can still use this with buttons or images.

I loop over each match and first get the table referenced, throwing an error in console if it doesn't exist:

links.forEach(l => {
	let table = document.querySelector(`#${l.dataset.table}`);
	if(!table) {
		console.error(`Unable to connect to table with id ${l.dataset.table}`);
		return;
	}

Next, I check the type, defaulting to text:

let type = l.dataset.copy || 'text';

The final bit is to assign the click handler:

l.addEventListener('click', async e => {
	e.preventDefault();
	console.log(`about to copy table ${table.id} to ${type}`);
	let data = getRawData(table);
	let formattedData = formatData(data, type);
	await navigator.clipboard.writeText(formattedData);
});

For each, I get the raw data from the HTML table, convert it to the right format, and then write to the clipboard. (Check out my article on working with the clipboard in JavaScript.)

getRawData just iterates through the table's DOM creating a 2D array:

const getRawData = t => {
	let data = [];
	t.querySelectorAll('tr').forEach(r => {
		let row = [];
		r.querySelectorAll('th, td').forEach(d => {
			row.push(d.innerText);
		});
		data.push(row);
	});
	return data;
}

formatData is a bit more complex and I used AI to help me with each of the main sections. I added support for plain text, Markdown, and CSV:

const formatData = (data,type) => {
	let numCols, colWidths, lines, buildRow, buildDivider;
	
	switch(type) {
		case "text": 

			numCols = Math.max(...data.map(r => r.length));
			// Compute max width for each column
			colWidths = Array(numCols).fill(0);
			for (const row of data) {
			for (let i = 0; i < numCols; i++) {
				const cell = row[i] !== undefined ? String(row[i]) : '';
				colWidths[i] = Math.max(colWidths[i], cell.length);
			}
			}
		
			buildDivider = () =>
			'+' + colWidths.map(w => '-'.repeat(w + 2)).join('+') + '+';
		
			buildRow = (row) =>
			'|' + colWidths.map((w, i) => {
				const cell = row[i] !== undefined ? String(row[i]) : '';
				return ' ' + cell.padEnd(w) + ' ';
			}).join('|') + '|';
		
			lines = [];
			lines.push(buildDivider());
			lines.push(buildRow(data[0])); // treat first row as header
			lines.push(buildDivider());
			for (let i = 1; i < data.length; i++) {
			lines.push(buildRow(data[i]));
			}
			lines.push(buildDivider());
		
			return lines.join('\n');

		case "md":
		case "markdown": 
			numCols = Math.max(...data.map(r => r.length));
			
			buildRow = (row) =>
				'| ' + Array.from({ length: numCols }, (_, i) =>
					row[i] !== undefined ? String(row[i]) : ''
				).join(' | ') + ' |';
		
			lines = [];
			lines.push(buildRow(data[0])); // header
			lines.push('| ' + Array(numCols).fill('---').join(' | ') + ' |');
			for (let i = 1; i < data.length; i++) {
				lines.push(buildRow(data[i]));
			}
		
			return lines.join('\n');

		case "csv": 
			const escapeCell = (value) => {
			const str = value !== undefined && value !== null ? String(value) : '';
			// Quote if it contains comma, quote, or newline
			if (/[",\n\r]/.test(str)) {
				return '"' + str.replace(/"/g, '""') + '"';
			}
			return str;
			};
		
			return data.map(row => row.map(escapeCell).join(',')).join('\n');
			
	}
}

With my test table (and I'm stripping out a bit here for brevity), here's the plain text version:

+----------+------------+--------+----------------------+
| Name     | DOB        | Gender | Breed                |
+----------+------------+--------+----------------------+
| Whiskers | 2019-03-14 | Male   | Maine Coon           |
| Luna     | 2020-07-22 | Female | Siamese              |
| Oliver   | 2018-11-05 | Male   | British Shorthair    |
| Max      | 2019-06-25 | Male   | Sphynx               |
| Stella   | 2021-05-04 | Female | American Curl        |
| Rocky    | 2018-09-20 | Male   | Burmese              |
| Molly    | 2022-01-11 | Female | LaPerm               |
+----------+------------+--------+----------------------+

Here's the Markdown:

| Name | DOB | Gender | Breed |
| --- | --- | --- | --- |
| Whiskers | 2019-03-14 | Male | Maine Coon |
| Luna | 2020-07-22 | Female | Siamese |
| Oliver | 2018-11-05 | Male | British Shorthair |
| Max | 2019-06-25 | Male | Sphynx |
| Stella | 2021-05-04 | Female | American Curl |
| Rocky | 2018-09-20 | Male | Burmese |
| Molly | 2022-01-11 | Female | LaPerm |

And here's CSV:

Name,DOB,Gender,Breed
Whiskers,2019-03-14,Male,Maine Coon
Luna,2020-07-22,Female,Siamese
Oliver,2018-11-05,Male,British Shorthair
Max,2019-06-25,Male,Sphynx
Stella,2021-05-04,Female,American Curl
Rocky,2018-09-20,Male,Burmese
Molly,2022-01-11,Female,LaPerm

I wrapped this all up in a function that you could add to your page. After adding it, don't forget to actually add the links/button/etc to enable it.

There's two issues with my code that come to mind, and I'd love people to comment in with their ideas. First, both the text and Markdown assume the first row is a header. In theory that might not always be accurate. I could update the code to see if the first cell in the first row is th versus td, but that kinda feels like overkill.

The second issue that comes to mind is user feedback. Usually these types of things will provide some kind of visual feedback ("Data copied!"). My code does not. I honestly don't quite know how I'd do that. Perhaps a data attribute that would replace the text (assuming it's a link or button) temporarily? I'm not sure.

If you've got ideas, leave me a comment below. Here's the demo:

See the Pen Copy Table As (2) by Raymond Camden (@cfjedimaster) on CodePen.

Read the whole story
alvinashcraft
51 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Announcing new builds for 20 July 2026

1 Share
Hello Windows Insiders, Today we are releasing new Windows 11 Insider Preview Builds across Beta, Experimental and Release Preview. See your channel release notes here: For those on other specific build versions, here are today’s new builds and release notes:
  • Beta (26H1): Build 28020.2539
  • Experimental (26H1): No new build today, please keep an eye out for future blog updates
  • Experimental (Future Platforms): No new build today, please keep an eye out for future blog updates
We also have Release Preview builds today for 24H2/25H2 and 26H1: Thanks, Stephen and the Windows Insider Program team
Read the whole story
alvinashcraft
51 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Using AI to Build a Blazor App 3: Prototypes and Tracer Bullets

1 Share

How do you figure out how your app idea should feel to use? Or which part to build first? Turns out we can draw inspiration from some classic texts on software development.

In Parts 1 and 2, we used AI to help us get clear about what we’re building and why. We scoped it down and were explicit about what’s not in scope. This left us with some decisions made and a starting point for the next step.

But what is that step? Well that depends slightly on what you’re trying to do.

In Part 1, we identified that software design is messy. Reaching a shared understanding about what you’re building (shared with other humans or AI) is a challenge. What we definitely don’t want to do is give AI a giant spec and tell it to “go build it.” There are too many unknowns and opportunities for AI to drift from what we really wanted in the first place.

So how do we break this down into smaller steps and stay agile so we can react to reality when it hits us in the face and demands we change direction?

It turns out there are answers to be found in the classic software development texts. The Pragmatic Programmer (a book you absolutely should read, especially if you’re using AI for coding) talks about tracer bullets and prototypes. They are two different tools for slightly different purposes.

Tracer Bullets

A tracer bullet is a bullet that’s loaded at intervals alongside regular bullets and leaves a trace when fired. The idea is it helps you locate the target (if you see the tracer bullets hitting a target, so are the other bullets).

In coding terms, we can use this to build something that gives you early feedback that you’re aiming at the right target.

In a project like this, a useful tracer bullet might be a minimal feature that exercises the entire stack (UI to backend to persistent storage). Or maybe that first stage of setting up the web project, running it and checking it loads in the browser.

The key with the tracer bullet is that it’s production code, built vertically, to verify your basic premise holds up.

Prototypes

Prototypes are different. They’re useful for testing out UI designs or spiking technical choices that you haven’t used before. You can use them for everything from generating multiple versions of a screen (UI only, no backend) to settle on a design/UX, to spiking out how Claude’s Agent SDK could work to give your app AI capabilities.

The big difference between prototypes and tracer bullets is that prototypes are throwaway. Once you’ve used them to clarify an aspect of your project, you chuck them in the bin. Tracer bullets, on the other hand, are small vertical slices of your app that you then keep and build on over time.

So Which Is It to Be?

In this case, I wanted to think through the UI and UX for this new app. Specifically to figure out the ergonomics of everything we decided was in scope (in Part 2).

Now I want to prototype this. To be clear, this prototype is for deciding on the rough shape of the UI for this feature. I would like to see a rough mockup (not over-designed) of how this article workspace could look. This is prototype code which will be thrown away, so does not need to be functional beyond showing me potential UI/UX

Again, we’re starting with a prompt to see how Codex (GPT 5.5) handles this. From that prompt, it created a basic V1, which was surprisingly functional but had a few issues.

First Article Workspace prototype with cramped Quick Capture, sidebar nav, editor and a sidelined newsletter panel

The issues here include:

  • The “Quick Capture” is too small to be useful (top-left).
  • The newsletter showing on the right (or underneath on smaller screens) feels a bit sidelined over there (again, too small).
  • Overall, this is trying to do much on one screen.

But the markdown editor works, as does the left-hand nav, and I liked the idea of the publish checks (which we had surfaced in earlier conversations with the AI).

So, over a few messages, we iterated this prototype:

Capture an idea that is way too small to be useful. I think I’d be tempted to have that as a button, and a modal in this case (not always a fan of a modal, but seems OK here)

It then built a new modal for capturing an idea, but it added more fields. This is a trap with LLMs trying to be helpful (drifting from the brief), so I brought it back.

we’ve drifted from the brief there, we agreed to simply capture a rough idea/title and also optionally why it matters. So the idea should be a text area (rough idea) too and we don’t need additional fields

After a few more back-and-forths, we arrived at a better version, but I wanted to check we’d met the requirements listed in the most recent ADR (created in the last step).

if you review the ADR, what does this screen not meet (in terms of the requirements we landed on)

After a couple more iterations we landed here:

Revised Article Workspace prototype with Article and Newsletter tabs sharing the editor pane and Publish Checks on the right

Key features that came out of this process:

  • “Capture idea” button (opens modal)
  • Shared “editor pane” for both Article and Newsletter (with tabs)
  • Consistent UI/UX across both tabs (with publish checks on right)
  • Same markdown editor used on both tabs
  • AI functionality on the newsletter tab (to generate hook and subject line ideas)

The key thing that jumped out for me here was the realization that, although the ADR captured the details of what we were aiming for, it was only during this “mockup” stage that some key design decisions were made. It’s another example of using AI to help you, the human, think.

Because this is throwaway code, it’s cheap to iterate, throw ideas around and think through the ergonomics of this feature. This is not 1:1 production code, but it helped us arrive at some key UI/UX decisions, and we can always use this as a reference point when we come to build the real thing. It will be up to us how closely we want the AI to stick to this “design.”

Now for the Tracer Bullet

With that UI sorted, the next step is to make this something that runs in the browser, and as quickly as possible. A perfect case for tracer bullets!

using the prototype we settled on, please identify candidates for our first tracer bullet and present them to me. This should be a tracer bullet as in the Pragmatic Programmer, something we can build that’s real, and will be the basis of our app

At this point, GPT gave me a few options:

  • Capture Idea To Real Draft
  • Article Plus Companion Newsletter Draft
  • Publish Draft To Blog
  • Open Existing Draft and Save Edits

As this new UI will need to work with MDX files in my Astro blog as the source of truth, I figured “Open Existing Draft and Save Edits” was a good first step (proves the architecture and immediately gives me a new editor for existing content).

GPT suggested this acceptance test:

Given an existing Astro MDX draft, I can open the app, edit the article body in a comfortable editor, save it, close the app, reopen it, and see the same edited draft loaded from the real Astro source file.

At this stage, we need something to build from (which is a little more detailed than that acceptance test).

Ok great let’s capture that. I feel like we should create a task/ticket/issue for this.

With that GPT created a task md file. Here’s part of it:

Markdown task file titled Task 0001 Open Existing Astro Draft And Save Edits showing Goal, User Story, and Scope sections

This is all about context. We want to give AI enough context to do the work, but not so much it has to start guessing and interpreting mountains of detail to find the parts it needs.

This is a good first step. Next time, we’ll build that tracer bullet for real.

Read the whole story
alvinashcraft
51 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Building a Healthcare UI with .NET MAUI

1 Share

You’ve probably seen a healthcare interface when you were scheduling a recent doctor’s appointment. Let’s see how to build one in .NET MAUI!

One of the biggest challenges when creating app interfaces is when design elements step a bit outside the conventional. These kinds of designs push us to think differently and get more creative when translating them into code.

In this article, I’m bringing you a healthcare UI inspired by this Dribbble doctor appointment UI design. I have to admit, it’s one of my favorites! It gives us the perfect opportunity to apply some cool tricks that make our design look impressive and truly out of the box.

We’ll be using some Progress Telerik UI for .NET MAUI components, along with a CollectionView, but this time, we’ll simulate different columns to achieve the layout we want. Plus, we’ll rely on three properties that will become our best friends in this UI: Rotation, TranslationY and TranslationX.

So grab your cup of coffee or hot chocolate ☕ and let’s build this together! ✨

How Will the Explanation Work?

So you can understand where we’re headed, I’ll explain how the process will work:

Visual diagram: Before we start, you’ll see a diagram with a screenshot of the design we want to achieve. This design will be divided into blocks, each identified with a name and a color. Each block will be explained individually.

Code explanation per block: In addition to the text explanation, each block will include the exact code snippet you need to write to achieve the result.

Step-by-step visual progress: Each explanation will have its corresponding screenshot, so you can see the progress as we build the UI.

⚠️ For this design, we took inspiration from an original Dribbble UI but made some adaptations and adjustments to align it with the goals of this article.

Environment Setup

You’ll learn how to take advantage of some of the powerful Telerik UI for .NET MAUI components in this post. If you don’t have the .NET MAUI library installed yet, visit the quick start page, which explains step-by-step how to install Telerik UI for .NET MAUI, download the license key, configure the Telerik package source and install the Telerik MCP server (if needed).

Breaking Down the Design into Blocks

To make this easier to follow, I’ve divided the design into clear blocks. We’ll build each part one at a time, in the order you see below.

Alright, now that we’ve broken our UI into clear blocks, it’s time to bring everything to life with some code! Let’s start building each section step by step.

Block 1. Intro

This first block is one of my favorites, as it brings a unique touch to the UI. It plays with the way the information is presented and introduces a slightly different style for the Get Started button. Since this block represents a full page, we’ll break this explanation down into the following steps:

Creating the Main Page with a Background Image

Let’s start by creating a page called DoctorIntroPage.xaml. This page will contain a Grid as the main layout. The grid will have only one row, which will allow us to play with the overlapping effect between the background image and the rest of the elements. Right there, you’ll also see exactly where the background image is added.

<Grid RowDefinitions="*"> 
    <!-- Main image -->
    <Image Grid.Row="0" Source="doctor.jpg" Aspect="AspectFill" Margin="0,40,0,0"/>
    
    <!-- Add the rest of the code for the first block -- >
</Grid>

Creating the Cards and the Get Started Button

To continue with these elements, we’ll add another Grid. This one will contain three rows and two columns:

<Grid Grid.Row="0" RowDefinitions="auto,*,auto,auto" ColumnDefinitions="*,*" Padding="20,40,20,30"> 
    <!-- Add the rotated cards here -- > 
    <!-- Add the Get Started button here -- > 
</Grid>

⚠️ Add the previous code exactly in the comment above that says <!-- Add the rest of the code for the first block -->.

Rotated Cards

Now, to add the cards, we’ll use one of my favorite Telerik controls: Telerik Border for .NET MAUI. Make sure you follow the steps indicated in the Environment Setup section at the beginning of the article. After that, you just need to add the corresponding namespace:

xmlns:telerik="clr-namespace:Telerik.Maui.Controls;assembly=Telerik.Maui.Controls"

To achieve the card effect, I mainly relied on the Rotation property. One card uses a value of 50, while the other uses -12, creating the effect of opposite rotations between them. In addition, I used the TranslationX and TranslationY properties to position the cards and make the overlapping effect more noticeable. In code, it would look like this:

<telerik:RadBorder Grid.Row="1" Grid.Column="0" 
    BackgroundColor="#f4f5fe" 
    CornerRadius="20" 
    HeightRequest="190" 
    WidthRequest="190" 
    TranslationX="20" 
    Rotation="5"> 
<Grid RowDefinitions="auto,*,auto" ColumnDefinitions="*,auto" Padding="15"> 
    <telerik:RadButton Grid.Row="0" Grid.Column="1" ImageSource="cardiogram" BackgroundColor="Black" HeightRequest="60" WidthRequest="60" CornerRadius="30"/> 
    <Label Grid.Row="1" Grid.Column="0" Text="Strengthen" FontAttributes="Bold" FontSize="16"/> 
    <Label Grid.Row="2" Grid.Column="0" Text="Support your heart with care and healthy lifestyle." FontSize="11" /> 
</Grid> 
</telerik:RadBorder> 
<telerik:RadBorder Grid.Row="1" Grid.Column="1" 
    BackgroundColor="#3d7ff5" 
    CornerRadius="20" 
    HeightRequest="190" 
    WidthRequest="190" 
    TranslationY="90" 
    TranslationX="-10" 
    Opacity="0.8" 
    Rotation="-12"> 
    <Grid RowDefinitions="auto,*,auto" ColumnDefinitions="auto,*" Padding="15"> 
    <telerik:RadButton Grid.Row="0" Grid.Column="0" ImageSource="stethoscope" BackgroundColor="White" HeightRequest="60" WidthRequest="60" CornerRadius="30"/> 
    <Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" TextColor="White" Text="Safeguard" FontAttributes="Bold" HorizontalTextAlignment="End" FontSize="16" /> 
    <Label Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" TextColor="White" Text="Reduce risks with regular check-ups and early ditention." HorizontalTextAlignment="Start" FontSize="11" /> 
    </Grid> 
</telerik:RadBorder>

Finally, Let’s Add the Get Started Button

To achieve this, we’ll use a Telerik Border as a container to create a rounded black background behind the button. Inside it, we’ll place a Telerik RadButton.

Since we’ve already added the namespace for RadBorder, you’re all set, there’s no need to add anything else to use RadButton.

<telerik:RadBorder Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" 
    BackgroundColor="Black" 
    Opacity="0.9" 
    CornerRadius="50" 
    HeightRequest="70" > 
    <Grid ColumnDefinitions="*,auto" Padding="15,5"> 
    <Label Grid.Column="0" Text="Get Started" FontSize="15" TextColor="White" VerticalTextAlignment="Center" /> 
    <telerik:RadButton Grid.Column="1" ImageSource="uprightarrow" BackgroundColor="#3d7ff5" HeightRequest="50" WidthRequest="50" CornerRadius="25" HorizontalOptions="End" Margin="10"/> 
    </Grid> 
</telerik:RadBorder>

We’ve finished this first block, and we already have our first page created! Now, let’s move on to the second block and keep building!

Block 2. Header

We’ve finished this first section, and our first page is already up and running! Now, let’s move on to the second section.

This time, we’re going to create a page called DoctorDetailsPage.xaml. Set the background color of this page to #f4f5fe and let’s define the main layout using a Grid. This Grid will have five rows and three columns, as shown below:

<Grid RowDefinitions="auto,auto,auto,auto,auto,*" 
    ColumnDefinitions="*,auto,auto" 
    Padding="20,30,20,0"> 
    <!-- Add the header block here -- > 
    <!-- Add the appointment block here -- > 
    <!-- Add the available doctors block here -- > 
</Grid>

Now, within the Grid, let’s add the name and email:

<Label Grid.Row="0" Grid.Column="0" Text="Ronalds S." FontSize="24" FontAttributes="Bold" />

<Label Grid.Row="1" Grid.Column="0" Text="ronalds@gmail.com" FontSize="14"/>

Next, we’ll add a Telerik RadBorder control (remember to include the corresponding namespace, just like we did on the previous page). Inside it, we’ll place an image along with a rounded button:

<telerik:RadBorder Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Grid.ColumnSpan="2" 
    Margin="0,0,0,40" 
    BackgroundColor="White" 
    CornerRadius="120" 
    VerticalOptions="Start"> 
    <HorizontalStackLayout> 
    <telerik:RadButton ImageSource="plus" BackgroundColor="#ebecfb" HeightRequest="60" WidthRequest="60" CornerRadius="30" Margin="5,2,0,1"/> 
    <Image Source="doctoravatar" HeightRequest="60" WidthRequest="60" Aspect="AspectFill" Margin="5,2"/> 
    </HorizontalStackLayout> 
</telerik:RadBorder>

Block 3. Appointment

The Appointment block contains different elements, which are achieved by adding a few Label controls along with a Telerik RadBorder, as shown below:

<Label Grid.Row="2" Grid.Column="0" Text="Today" FontSize="14" /> 
<Label Grid.Row="3" Grid.Column="0" Text="Appointment" FontAttributes="Bold" FontSize="16" /> 
<Label Grid.Row="2" Grid.Column="2" Text="See all" HorizontalTextAlignment="End" TextColor="#3d7ff5" FontSize="14" />
 
<telerik:RadBorder Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="3" 
    BackgroundColor="#3d7ff5" 
    CornerRadius="120" 
    Margin="0,10,0,0" 
    HeightRequest="80" AutomationProperties.IsInAccessibleTree="True"> 
    <Grid ColumnDefinitions="auto,*" RowDefinitions="auto,auto,auto" VerticalOptions="Center"> 
    <Image Grid.Column="0" Grid.Row="0" Grid.RowSpan="3" Source="doctorjose" HeightRequest="80" WidthRequest="80" Aspect="AspectFill" Margin="5,2"/> 
    <Label Grid.Column="1" Grid.Row="0" Text="March 21, 10:00 AM" FontSize="10" TextColor="White" VerticalTextAlignment="End" /> 
    <Label Grid.Column="1" Grid.Row="1" Text="Dr. Suzanne Holroyd" FontSize="16" FontAttributes="Bold" TextColor="White"/> 
    <Label Grid.Column="1" Grid.Row="2" Text="Pulmonology" FontSize="10" TextColor="White" VerticalTextAlignment="Start" /> 
    </Grid>
</telerik:RadBorder>

Block 4. Available Doctors

For our final block, we’ll also use a Telerik RadBorder to simulate a white background with rounded top corners. Inside it, we’ll add a Grid with two rows and three columns. Within this layout, we’ll include the title label, two rounded buttons and, finally, a CollectionView.

<telerik:RadBorder Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="3" 
VerticalOptions="Fill" 
BackgroundColor="White" 
CornerRadius="30,30,0,0" 
Margin="-20,20,-20,0">

<Grid RowDefinitions="auto,*" ColumnDefinitions="*,auto,auto" Padding="20,30,0,0"> 
    <Label Grid.Row="0" Grid.Column="0" Text="Select &#10;Visit Type" FontSize="15" VerticalTextAlignment="End" FontAttributes="Bold"/> 
    <telerik:RadButton Grid.Row="0" Grid.Column="1" ImageSource="search" BackgroundColor="#ebecfb" HeightRequest="50" WidthRequest="50" CornerRadius="25" Margin="0,0,5,0" /> 
    <telerik:RadButton Grid.Row="0" Grid.Column="2" ImageSource="filter" BackgroundColor="#ebecfb" HeightRequest="50" WidthRequest="50" CornerRadius="25" />
    <CollectionView Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" 
    ItemsSource="{Binding specialists}"> 
    <CollectionView.ItemsLayout> 
    <GridItemsLayout Orientation="Horizontal" HorizontalItemSpacing="15" Span="2"/> 
    </CollectionView.ItemsLayout> 
    <CollectionView.ItemTemplate> 
    <DataTemplate> 
    <telerik:RadBorder BackgroundColor="{Binding bgColor}" 
    CornerRadius="20" 
    HeightRequest="150" 
    WidthRequest="150"> 
    <Grid RowDefinitions="auto,auto,*,*" ColumnDefinitions="*,auto" Padding="15"> 
    <telerik:RadButton Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" ImageSource="rightupblack" BackgroundColor="{Binding btnColor}" HeightRequest="50" WidthRequest="50" CornerRadius="25"/> 
    <Label Grid.Row="0" Grid.Column="0" Text="{Binding Price}" FontAttributes="Bold" FontSize="16" TextColor="{Binding txColor}"/> 
    <Label Grid.Row="1" Grid.Column="0" Text="Per visit" FontAttributes="Bold" FontSize="10" TextColor="{Binding txColor}"/> 
    <Label Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding category}" TextColor="{Binding txColor}" FontAttributes="Bold" FontSize="12" VerticalTextAlignment="End"/> 
    <Label Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding description}" TextColor="{Binding txColor}" FontAttributes="Bold" FontSize="10"/> 
    </Grid> 
    </telerik:RadBorder> 
    </DataTemplate> 
    </CollectionView.ItemTemplate> 
    </CollectionView> 
</Grid> 
</telerik:RadBorder>

✍️ Yes! You might notice something different in the CollectionView. Instead of a traditional list, the items are displayed in multiple columns. This is achieved by using a GridItemsLayout and setting the Span property, which allows us to organize the results into columns.

And that’s it—we’re all done! Below, you can see the final result of both screens.


Conclusion

And that’s it! In this article, we explored how to build a doctor’s office app UI using .NET MAUI with XAML. We walked through how to structure the UI into blocks, create designs using grids, apply overlapping effects and enhance the experience using Telerik components like RadBorder and RadButton.

I hope this article helps you take these ideas and apply them to your own projects!

If you have any questions or would like me to dive deeper into a specific part, feel free to leave a comment—I’ll be happy to help!  See you in the next article! ‍♀️

Remember, Telerik UI for .NET MAUI comes with a free 30-day trial. So you can play around in the meantime!

Try Now

Read the whole story
alvinashcraft
51 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories