📱 Building a Reusable Image Control with 🔄 Loading Indicator in .NET MAUI 🚀

Ali RazaAli Raza
3 min read

🚀 Introduction

When developing modern mobile apps, user experience is key—especially when working with images loading from the internet. A smooth and responsive image loading experience can make a big difference in how polished your app feels.

That’s why I created a reusable custom control in .NET MAUI that shows a loading spinner while the image is being fetched and rendered. This post walks you through how I built this lightweight control and how you can easily plug it into your own MAUI apps.

🔧 Use Case

Imagine you're building an app where dozens of images are loaded dynamically. Without a visual cue, users might think your app is unresponsive. That’s where a loading indicator over an image becomes a subtle yet powerful UX enhancement.

💡 How It Works

We define an Grid that layers the Image and the ActivityIndicator. We bind the ActivityIndicator to the IsLoading property of the Image, so it auto-reacts to the image's loading state.

<Grid xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                x:Class="MauiControls.Controls.ImageView.ImageLoading"
                x:Name="this">

    <Image x:Name="img"
           Source="{Binding Source={x:Reference this}, Path=Source}"/>

    <ActivityIndicator x:Name="activityIndicator"
                       Color="{StaticResource Secondary}"
                       HeightRequest="20"
                       WidthRequest="20"
                       IsRunning="{Binding IsLoading, Source={x:Reference img}}"
                       IsEnabled="{Binding IsLoading, Source={x:Reference img}}"
                       IsVisible="{Binding IsLoading, Source={x:Reference img}}"
                       Opacity="0.9"/>
</Grid>

The backing C# class exposes bindable properties for flexible integration:

 namespace MauiControls.Controls.ImageView;

public partial class ImageLoading : Grid
{
    #region Constructor
    public ImageLoading()
    {
        InitializeComponent();
    }
    #endregion

    #region BindableProperties
    public static readonly BindableProperty SourceProperty = BindableProperty.Create(
        propertyName: nameof(Source),
        returnType: typeof(string),
        declaringType: typeof(ImageLoading),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneTime);

    public string Source
    {
        get => (string)GetValue(SourceProperty);
        set => SetValue(SourceProperty, value);
    }

    public static readonly BindableProperty ImageAspectProperty = BindableProperty.Create(
        propertyName: nameof(ImageAspect),
        returnType: typeof(Aspect),
        declaringType: typeof(ImageLoading),
        defaultValue: Aspect.AspectFit,
        defaultBindingMode: BindingMode.OneTime,
        propertyChanged: OnAspectChanged);

    public Aspect ImageAspect
    {
        get => (Aspect)GetValue(ImageAspectProperty);
        set => SetValue(ImageAspectProperty, value);
    }

    public static readonly BindableProperty ImageHeightProperty = BindableProperty.Create(
        propertyName: nameof(ImageHeight),
        returnType: typeof(int),
        declaringType: typeof(ImageLoading),
        defaultValue: 0,
        defaultBindingMode: BindingMode.OneTime,
        propertyChanged: OnHeightChanged);

    public int ImageHeight
    {
        get => (int)GetValue(ImageHeightProperty);
        set => SetValue(ImageHeightProperty, value);
    }

    public static readonly BindableProperty ImageWidthProperty = BindableProperty.Create(
        propertyName: nameof(ImageWidth),
        returnType: typeof(int),
        declaringType: typeof(ImageLoading),
        defaultValue: 0,
        defaultBindingMode: BindingMode.OneTime,
        propertyChanged: OnWidthChanged);

    public int ImageWidth
    {
        get => (int)GetValue(ImageWidthProperty);
        set => SetValue(ImageWidthProperty, value);
    }

    public static readonly BindableProperty ImageStyleProperty = BindableProperty.Create(
        propertyName: nameof(ImageStyle),
        returnType: typeof(Style),
        declaringType: typeof(ImageLoading),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneTime,
        propertyChanged: OnStyleChanged);

    public Style ImageStyle
    {
        get => (Style)GetValue(ImageStyleProperty);
        set => SetValue(ImageStyleProperty, value);
    }
    #endregion

    #region Events

    static void OnStyleChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is ImageLoading view && newValue is Style newStyle)
            view.img.Style = newStyle;
    }

    static void OnWidthChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is ImageLoading view && newValue is int newWidth)
            view.img.WidthRequest = newWidth;
    }

    static void OnHeightChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is ImageLoading view && newValue is int newHeight)
            view.img.HeightRequest = newHeight;
    }

    static void OnAspectChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is ImageLoading view && newValue is Aspect newAspect)
            view.img.Aspect = newAspect;
    }
    #endregion
}

⚙️ Bonus Features

  • Aspect Ratio Control: Maintain visual consistency.

  • 📐 Custom Sizing: Set width and height directly.

  • 🎨 Style Support: Apply any defined style to your image.

🔌 How to Use It

Simply include the control in your XAML like so:

<controls:ImageLoading 
    Source="https://example.com/image.jpg"
    ImageAspect="AspectFill"
    ImageHeight="200"
    ImageWidth="200" />

And your images now have built-in loading indicators, improving perceived performance with minimal code.

🧠 Final Thoughts

.NET MAUI empowers us to write clean, reusable UI components that elevate user experience across platforms. This ImageLoading control is just one example of how small UI enhancements can have a big impact.

Stay tuned—I’ll continue sharing practical .NET MAUI patterns and performance tips in upcoming posts.

0
Subscribe to my newsletter

Read articles from Ali Raza directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ali Raza
Ali Raza

🚀 Tech Lead | .NET MAUI Expert | Mobile App Developer I'm Ali Raza, a passionate Tech Lead with over 6 years of experience in mobile app development. I specialize in .NET MAUI/Xamarin and have led multiple high-impact projects, including enterprise apps, fintech solutions, and eSIM technology. 🔹 What I Do: ✔ .NET MAUI & Xamarin – Building cross-platform apps with robust architectures ✔ eSIM & Fintech Apps – Leading innovations in digital connectivity & finance ✔ Performance Optimization – Creating high-quality, scalable mobile solutions ✔ Mentorship & Community Sharing – Helping developers master .NET MAUI 📢 Sharing Weekly Insights on .NET MAUI/Xamarin to help developers level up! Follow me for deep dives into Native Interop, API Optimization, and Advanced UI/UX techniques. Let’s connect and build something amazing! 🚀