Create a Beautiful Photo Gallery Using .NET MAUI Tab View and ListView

syncfusionsyncfusion
10 min read

TL;DR: Want to build a sleek photo gallery in your .NET MAUI app? Learn how to combine Syncfusion’s Tab View and ListView controls to create a visually appealing, organized gallery. Follow step-by-step instructions to display, group, and navigate photos and albums with dynamic data binding, interactive layouts, and custom styles. Perfect for showcasing images with smooth performance and intuitive UI.

In today’s mobile-first world, users expect visually rich apps. Especially when it comes to photos. Whether it’s travel memories or event captures, organizing images in a sleek and interactive gallery enhances user experience.

In this tutorial, you’ll learn how to build a stunning photo gallery app using .NET MAUI and Syncfusion’s Tab View and ListView controls. We’ll guide you through step-by-step implementation with dynamic data, grouped layouts, and custom styles, optimized for performance across Android, iOS, and Windows.

Why use Syncfusion .NET MAUI Tab View?

The .NET MAUI Tab View provides:

  • Navigation: An intuitive way to organize and navigate through different parts of your app.

  • Customization: Enables unique and attractive UI through customizable headers and content areas.

  • Swiping: Seamless swipe functionality to switch between tabs.

Why use Syncfusion .NET MAUI ListView?

The .NET MAUI List View is excellent for building a photo gallery because:

  • Item templates: It supports rich UI through item templating.

  • Layouts: Supports both linear and grid layouts, ideal for a gallery.

  • Grouping: Offers customizable group headers with sticky views.

Follow these steps to create your stunning photo gallery:

1. Create a simple .NET MAUI Tab View

Start by creating a MAUI application. Refer to the documentation for detailed instructions on setup. Include the following basic Tab View:

<tabView:SfTabView />

2. Add multiple tabs with headers

Add tabs with different headers to categorize your images:

<tabView:SfTabView x:Name="tabView" TabBarPlacement="Bottom">
    <tabView:SfTabView.Items>
        <tabView:SfTabItem Header="Photos" ImageSource="photos.png" />
        <tabView:SfTabItem Header="Albums" ImageSource="albums.png" />
        <tabView:SfTabItem Header="Favorites" ImageSource="favorites.png" />
    </tabView:SfTabView.Items>
</tabView:SfTabView>

3 Add ListView as content for each tab

You can refer to the documentation to create the Maui ListView. Include a List View for displaying photos within each tab. Here’s how you can set it up:

<tabView:SfTabItem Header="Photos" ImageSource="photos.png">
    <tabView:SfTabItem.Content>
        <listView:SfListView>
            <listView:SfListView.ItemTemplate>
                <DataTemplate>  
                    <!-- Bind your ImageData here -->
                </DataTemplate>
            </listView:SfListView.ItemTemplate>
        </listView:SfListView>
    </tabView:SfTabItem.Content>
</tabView:SfTabItem>

4. Populate items in the ListView

1. Define model classes

Create model classes to hold image data, such as ImageInfo and AlbumInfo. Define properties related to each photo:

ImageInfo.cs

public class ImageInfo : INotifyPropertyChanged
{
    private string imageName;
    private string image;
    private string size;
    private DateTime dateTime;
    private bool isFavorite;

    // Properties with Change Notification
    public string ImageName
    {
        get => imageName;
        set
        {
            imageName = value;
            OnPropertyChanged();
        }
    }

    // Similar properties for other fields...

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

AlbumInfo.cs

public class AlbumInfo
{
    public string AlbumName { get; set; }
    public string CoverImage { get; set; }
    public double Count { get; set; }
    public ObservableCollection<ImageInfo> Photos { get; set; }
}

2. Create a ViewModel to manage your data

Use a GalleryViewModel class to generate and manage collections of images and albums.

GalleryViewModel.cs

public class GalleryViewModel
{
    public ObservableCollection<ImageInfo> Photos { get; set; }
    public ObservableCollection<ImageInfo> Countries { get; set; }
    public ObservableCollection<ImageInfo> Birds { get; set; }
    public ObservableCollection<ImageInfo> Electronics { get; set; }
    public ObservableCollection<ImageInfo> Foods { get; set; }
    public ObservableCollection<ImageInfo> Recents { get; set; }
    public ObservableCollection<ImageInfo> Favorites { get; set; }
    public ObservableCollection<AlbumInfo> Albums { get; set; }

    public GalleryViewModel()
    {
        GeneratePhotos();
        GenerateAlbums();
    }

    // Method to generate a collection of images
    private void GeneratePhotos()
    {
        DateTime dateTime = DateTime.Now.Date;
        Photos = new ObservableCollection<ImageInfo>
        {
            new ImageInfo { ImageName = "place_1", Image = "place_1.jpg", Size = "2MB", DateTime = dateTime.AddHours(15) },
            new ImageInfo { ImageName = "bird01", Image = "bird01.png", Size = "2MB", DateTime = dateTime.AddDays(-1).AddHours(8) },
            new ImageInfo { ImageName = "India", Image = "india.jpg", Size = "2MB", DateTime = dateTime.AddDays(-27).AddHours(10), IsFavorite = true },
            // Additional images...
        };

        // Consolidate images into a single 'Recents' collection
        Recents = new ObservableCollection<ImageInfo>();
        foreach (var category in new[] { Photos, Countries, Birds, Foods, Electronics })
        {
            Recents = new ObservableCollection<ImageInfo>(Recents.Concat(category));
        }

        // Sort recent images by date
        var sortedRecentList = Recents.OrderByDescending(item => item.DateTime).ToList();
        Recents.Clear();
        foreach (var item in sortedRecentList)
        {
            Recents.Add(item);
        }

        // Initialize favorites
        Favorites = new ObservableCollection<ImageInfo>();
        foreach (var photo in Recents)
        {
            if (photo.IsFavorite)
            {
                Favorites.Add(photo);
            }
        }
    }

    // Method to generate albums
    private void GenerateAlbums()
    {
        Albums = new ObservableCollection<AlbumInfo>
        {
            new AlbumInfo { AlbumName = "Recents", CoverImage = "place_1.jpg", Count = 58, Photos = Recents },
            new AlbumInfo { AlbumName = "Countries", CoverImage = "argentina.jpg", Count = 13, Photos = Countries },
            new AlbumInfo { AlbumName = "Electronics", CoverImage = "blindinglights.png", Count = 8, Photos = Electronics },
            new AlbumInfo { AlbumName = "Birds", CoverImage = "bird01.png", Count = 10, Photos = Birds },
            new AlbumInfo { AlbumName = "Foods", CoverImage = "bread.png", Count = 15, Photos = Foods },
        };
    }
}

5. Bind the image collection to the ListView

In your MainPage.xaml, define the layout and bind your image data to display within the ListView:

<tabView:SfTabView x:Name="tabView" TabBarPlacement="Bottom">

    <!-- Photos Tab -->
    <tabView:SfTabItem Header="Photos" ImageSource="photos.png">
        <listView:SfListView
            x:Name="listViewPhotos"
            ItemsSource="{Binding Photos}"
            SelectionMode="None"
            ItemSize="90"
            ItemSpacing="5">

            <listView:SfListView.ItemsLayout>
                <listView:GridLayout SpanCount="4" />
            </listView:SfListView.ItemsLayout>

            <listView:SfListView.ItemTemplate>
                <DataTemplate>
                    <Image
                        Source="{Binding Image}"
                        Aspect="AspectFill"
                        HeightRequest="88"
                        WidthRequest="88" />
                </DataTemplate>
            </listView:SfListView.ItemTemplate>

        </listView:SfListView>
    </tabView:SfTabItem>

    <!-- Albums Tab -->
    <tabView:SfTabItem Header="Albums" ImageSource="albums.png">
        <listView:SfListView
            ItemsSource="{Binding Albums}"
            ItemSize="230"
            SelectionMode="None">

            <listView:SfListView.ItemsLayout>
                <listView:GridLayout SpanCount="2" />
            </listView:SfListView.ItemsLayout>

            <listView:SfListView.ItemTemplate>
                <DataTemplate>
                    <Grid RowDefinitions="*,20,20">
                        <Image
                            Source="{Binding CoverImage}"
                            Aspect="Fill"
                            HeightRequest="180"
                            WidthRequest="180" />

                        <Label
                            Text="{Binding AlbumName}"
                            FontSize="15"
                            Margin="10,0,0,0"
                            Grid.Row="1" />

                        <Label
                            Text="{Binding Count, StringFormat='{0} items'}"
                            Margin="10,0,0,0"
                            FontSize="12"
                            Grid.Row="2" />
                    </Grid>
                </DataTemplate>
            </listView:SfListView.ItemTemplate>

        </listView:SfListView>
    </tabView:SfTabItem>

    <!-- Favorites Tab -->
    <tabView:SfTabItem Header="Favorites" ImageSource="favorites.png">
        <listView:SfListView
            ItemsSource="{Binding Favorites, Mode=TwoWay}"
            ItemSize="300"
            ItemSpacing="10"
            SelectionMode="None"
            AutoFitMode="Height">

            <listView:SfListView.ItemsLayout>
                <listView:GridLayout SpanCount="2" />
            </listView:SfListView.ItemsLayout>

            <listView:SfListView.ItemTemplate>
                <DataTemplate>
                    <Grid RowDefinitions="30,*">
                        <Label
                            Text="{Binding DateTime, StringFormat='{0:ddd, dd MMM, yyyy}'}"
                            FontAttributes="Bold"
                            FontSize="15" />

                        <Image
                            Source="{Binding Image}"
                            Aspect="AspectFill"
                            Grid.Row="1" />
                    </Grid>
                </DataTemplate>
            </listView:SfListView.ItemTemplate>

        </listView:SfListView>
    </tabView:SfTabItem>

</tabView:SfTabView>

Photos

Photos

Albums

Albums

6. Create pages to display images

Step 1: Create a page to display an individual image

You need a simple content page to show a single image when a user selects it from your photo gallery.

ImagePage.Xaml

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="PhotoGallery.ImagePage"
    Title="Image Details">

    <Grid RowDefinitions="*,60">
        <Image
            Source="{Binding Image}"
            Aspect="AspectFit" />
    </Grid>

</ContentPage>

Step 2: Create a page to display images from an album

To show a collection of images within an album, create another content page. This will use a List to display all images associated with a selected album.

AlbumCollectionPage.Xaml

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:listView="clr-namespace:Syncfusion.Maui.ListView;assembly=Syncfusion.Maui.ListView"
    x:Class="PhotoGallery.AlbumCollectionPage"
    Title="{Binding AlbumName}">

    <Grid>

        <listView:SfListView
            x:Name="listView"
            ItemsSource="{Binding Photos}"
            SelectionMode="None"
            ItemSize="90"
            ItemSpacing="5">

            <listView:SfListView.ItemsLayout>
                <listView:GridLayout SpanCount="4" />
            </listView:SfListView.ItemsLayout>

            <listView:SfListView.ItemTemplate>
                <DataTemplate>
                    <Image
                        Source="{Binding Image}"
                        Aspect="AspectFill"
                        HeightRequest="88"
                        WidthRequest="88" />
                </DataTemplate>
            </listView:SfListView.ItemTemplate>

        </listView:SfListView>

    </Grid>

</ContentPage>

7. Implementing navigation with tapped events

Step 1: Define the ItemTapped event in XAML

In your XAML file, define the ItemTapped event in ListView for navigation. This allows users to navigate to a detailed view of the photo when they tap on an image in the list.

MainPage.Xaml

<tabView:SfTabView x:Name="tabView" TabBarPlacement="Bottom">

    <tabView:SfTabItem 
        x:Name="photosTab" 
        Header="Photos" 
        ImageSource="photos.png" 
        ImageTextSpacing="5">

        <!-- ListView for displaying photos -->
        <listView:SfListView 
            x:Name="listViewPhotos" 
            ItemsSource="{Binding Photos}" 
            SelectionMode="None"
            ItemSize="90" 
            ItemSpacing="5" 
            ItemTapped="OnPhotosItemTapped">

            <!-- ItemTemplate and other configurations -->

        </listView:SfListView>

    </tabView:SfTabItem>

    ...

</tabView:SfTabView>

Step 2: Implement navigation in the code-behind

In your code-behind file, implement the logic to navigate to the image details page when an item is tapped.

MainPage.Xaml.cs

private void OnPhotosItemTapped(object sender, Syncfusion.Maui.ListView.ItemTappedEventArgs e)
        {
            // Create an instance of ImagePage
            ImagePage imagePage = new ImagePage(this.BindingContext as GalleryViewModel, e.DataItem as ImageInfo); 
            imagePage.BindingContext = e.DataItem as ImageInfo; // Pass the selected image to the new page

            // Navigate to the ImagePage
            Navigation.PushAsync(imagePage);
        }

8. Customizing the Tab View for selection

Step 1: Create a style for TabViewItem

Define styles to customize the text color of TabViewItem based on the selection state using visual states.

MainPage.Xaml

<ContentPage.Resources>
        <Style TargetType="tabView:SfTabItem">
            <Setter Property="VisualStateManager.VisualStateGroups">
                <VisualStateGroupList>
                    <VisualStateGroup>

                        <VisualState x:Name="Normal">
                            <VisualState.Setters>
                                <Setter Property="TextColor" Value="#000000" />
                            </VisualState.Setters>
                        </VisualState>

                        <VisualState x:Name="Selected">
                            <VisualState.Setters>
                                <Setter Property="TextColor" Value="#6750A4" />
                            </VisualState.Setters>
                        </VisualState>

                    </VisualStateGroup>
                </VisualStateGroupList>
            </Setter>
        </Style>
    </ContentPage.Resources>

Step 2: Customize the tab header

Customize the tab header’s appearance and behavior by setting properties such as IndicatorStrokeThickness, TabBarHeight, and using FontImageSource for icons.

MainPage.Xaml

<tabView:SfTabView 
    x:Name="tabView" 
    TabBarPlacement="Bottom" 
    EnableSwiping="True" 
    IndicatorStrokeThickness="0" 
    TabBarHeight="55">

    <tabView:SfTabItem 
        Header="Photos" 
        x:Name="photosTab" 
        ImageTextSpacing="5">

        <tabView:SfTabItem.ImageSource>
            <FontImageSource 
                Glyph="" 
                Color="{Binding Source={x:Reference photosTab}, Path=TextColor}" 
                FontFamily="PhotoGallery"/>
        </tabView:SfTabItem.ImageSource>

    </tabView:SfTabItem>

    <tabView:SfTabItem 
        Header="Albums" 
        x:Name="albumsTab" 
        ImageTextSpacing="5">

        <!-- Similar setup for other tabs -->

    </tabView:SfTabItem>

</tabView:SfTabView>

9. Group the ListView images

Group images in your ListView based on a property, such as DateTime, by using GroupDescriptor.

MainPage.Xaml:

public MainPage()
        {
            InitializeComponent();

            listViewPhotos.DataSource.GroupDescriptors.Add(new GroupDescriptor()
            {
                PropertyName = "DateTime",
                KeySelector = (object obj1) =>
                {
                    var item = obj1 as ImageInfo;
                    if (item != null)
                    {
                        if (item.DateTime.Date == DateTime.Now.Date)
                        {
                            return "Today";
                        }
                        else if (item.DateTime.Date == DateTime.Now.Date.AddDays(-1))
                        {
                            return "Yesterday";
                        }
                        else if (item.DateTime.Year == DateTime.Now.Year)
                        {
                            return item.DateTime.ToString("ddd, dd MMM", CultureInfo.InvariantCulture);
                        }
                        else
                        {
                            return item.DateTime.ToString("ddd, dd MMM, yyyy", CultureInfo.InvariantCulture);
                        }
                    }
                    else
                    {
                        return "";
                    }
                }
            });

        }

<alt-text>

Grouping the ListView images

10. Customize the image display page

Step 1: Create a converter for favorite icon

Implement a converter to change the display of favorite icons based on whether an image is marked as a favorite.

FavoriteToIconConverter

public class FavoriteToIconConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool isFavorite)
            {
                // Assume '' is the outline and '' is the filled icon.
                return isFavorite ? "\uE707" : "\uE706";
            }
            return "\uE706"; // Default to outline
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

Step 2: Handle property changes for adding to favorites

Update property change events in your ViewModel to handle the dynamic addition and removal of images from the favorites collection.

GalleryViewModel.cs

private void GeneratePhotos()
{
    ...
    foreach (var category in new[] { Photos, Countries, Birds, Foods, Electronics })
    {
        foreach (var photo in category)
        {
            photo.PropertyChanged += ImageInfo_PropertyChanged;
        }

        // Add to Recents
        Recents = new ObservableCollection<ImageInfo>(Recents.Concat(category));
    }
    ...
}

private void ImageInfo_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (sender is ImageInfo imageInfo && e.PropertyName == nameof(ImageInfo.IsFavorite))
    {
        if (imageInfo.IsFavorite && !Favorites.Contains(imageInfo))
        {
            Favorites.Add(imageInfo);
        }
        else if (!imageInfo.IsFavorite && Favorites.Contains(imageInfo))
        {
            Favorites.Remove(imageInfo);
        }
    }
}

Step 3: Implement UI actions in the image display page

Use the converter in your XAML to update the UI and define actions for user interactions like tapping icons.

ImagePage.Xaml

<ContentPage.Resources>
    <local:FavoriteToIconConverter x:Key="FavoriteToIconConverter"/>
</ContentPage.Resources>

<Grid RowDefinitions="*,60">
    <Image Source="{Binding Image}" Aspect="AspectFit"/>

    <HorizontalStackLayout VerticalOptions="End" Spacing="20" HorizontalOptions="Center">
        <!-- Favorite Button -->
        <Border StrokeThickness="0" HeightRequest="60" WidthRequest="60">
            <Border.StrokeShape>
                <RoundRectangle CornerRadius="30"/>
            </Border.StrokeShape>
            <effects:SfEffectsView TouchDownEffects="Highlight">
                <effects:SfEffectsView.GestureRecognizers>
                    <TapGestureRecognizer Tapped="OnFavoriteTapped"/>
                </effects:SfEffectsView.GestureRecognizers>
                <VerticalStackLayout HorizontalOptions="Center" VerticalOptions="Center">
                    <Label Text="{Binding IsFavorite, Converter={StaticResource FavoriteToIconConverter}}"
                           FontFamily="sPhotoGallery"
                           FontSize="24"
                           HorizontalOptions="Center"/>
                    <Label Text="Favorite" />
                </VerticalStackLayout>
            </effects:SfEffectsView>
        </Border>

        <!-- Delete Button -->
        <Border StrokeThickness="0" HeightRequest="60" WidthRequest="60">
            <Border.StrokeShape>
                <RoundRectangle CornerRadius="30"/>
            </Border.StrokeShape>
            <effects:SfEffectsView TouchDownEffects="Highlight">
                <effects:SfEffectsView.GestureRecognizers>
                    <TapGestureRecognizer Tapped="OnDeleteTapped"/>
                </effects:SfEffectsView.GestureRecognizers>
                <VerticalStackLayout HorizontalOptions="Center" VerticalOptions="Center">
                    <Label Text=""
                           FontFamily="PhotoGallery"
                           FontSize="24"
                           HorizontalOptions="Center"/>
                    <Label Text="Delete" />
                </VerticalStackLayout>
            </effects:SfEffectsView>
        </Border>
        ...
    </HorizontalStackLayout>
</Grid>

ImagePage.Xaml.cs

private void OnFavoriteTapped(object sender, TappedEventArgs e)
{
    if (BindingContext is ImageInfo imageInfo)
    {
        imageInfo.IsFavorite = !imageInfo.IsFavorite;
    }
}

private async void OnDeleteTapped(object sender, TappedEventArgs e)
{
    if (BindingContext is ImageInfo imageInfo)
    {
        // Confirm the deletion action
        bool isConfirmed = await DisplayAlert("Delete Image", "Are you sure you want to delete this image?", "Yes", "No");
        if (isConfirmed)
        {
            if (_viewModel.Recents.Contains(imageInfo))
                _viewModel.Recents.Remove(imageInfo);

            // Remove from Photos collection
            if (_viewModel.Photos.Contains(imageInfo))
                _viewModel.Photos.Remove(imageInfo);
            else if (_viewModel.Countries.Contains(imageInfo))
                _viewModel.Countries.Remove(imageInfo);
            else if (_viewModel.Foods.Contains(imageInfo))
                _viewModel.Foods.Remove(imageInfo);
            else if (_viewModel.Electronics.Contains(imageInfo))
                _viewModel.Electronics.Remove(imageInfo);
            else if (_viewModel.Birds.Contains(imageInfo))
                _viewModel.Birds.Remove(imageInfo);

            // Remove from Favorites if it's there
            if (imageInfo.IsFavorite)
            {
                _viewModel.Favorites.Remove(imageInfo);
            }

            // Navigate back to the main page or previous navigation stack
            await Navigation.PopAsync();
        }
    }
}

Implementing UI actions in the image display page

Implementing UI actions in the image display page

UI Actions

.NET MAUI Tab View and ListView

.NET MAUI Tab View and ListView

GitHub references

For more details, refer to the GitHub demo

Conclusion

Thanks for reading! With Syncfusion’s Tab View and ListView controls, building a responsive, interactive photo gallery in .NET MAUI becomes effortless. You’ve now seen how to group photos, display albums, and bind dynamic image data using modern UI components.

This approach not only improves user experience but also ensures your app is scalable and visually appealing across devices

Download Essential Studio for .NET MAUI to start evaluating the latest features immediately.

If you have any questions, you can also contact us through our support forum, support portal, or feedback portal. We are always happy to help you!

##Related Blogs

This article was originally published at Syncfusion.com.

0
Subscribe to my newsletter

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

Written by

syncfusion
syncfusion

Syncfusion provides third-party UI components for React, Vue, Angular, JavaScript, Blazor, .NET MAUI, ASP.NET MVC, Core, WinForms, WPF, UWP and Xamarin.