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.
The .NET MAUI Tab View provides:
The .NET MAUI List View is excellent for building a photo gallery because:
Follow these steps to create your stunning photo gallery:
Start by creating a MAUI application. Refer to the documentation for detailed instructions on setup. Include the following basic Tab View:
<tabView:SfTabView />
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>
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>
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; } }
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 }, }; } }
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 | Albums |
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>
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>
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>
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); }
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>
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>
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 ""; } } }); }
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(); } }
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); } } }
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(); } } }
UI Actions
For more details, refer to the GitHub demo
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!