MAUI C# Modal Freezing and Not Closing Automatically After Update/Add Operation

I am working on a MAUI C# application dedicated to Windows. This app is unique in that it works with a local database and calls an API that populates a database on a server. In this app, I have a page that displays a list of items. On this page, I also have buttons that allow me to add/update/delete items in this list. The buttons bring up a modal for updating or adding items.

When I click one of the buttons, the modal appears, and I make my changes. Since the modal’s background is transparent, I can see that the modification is applied to the list. However, the modal does not close automatically as intended in the code. I have to click on ‘exit’ for the modal to disappear.

Once I return to the list, if I want to interact with the list again, I click on one of the buttons. The modal opens, I perform my action (add/update), I see the modification taking place, but the modal does not close and freezes. If I click on ‘exit’, I get this error: Application.Current.MainPage.Navigation.ModalStack.Count = 0 and the application crashes.

I’ve tried quite a few things, but I feel like I’m missing something obvious in my main thread management. I don’t know threading in MAUI very well. Can anyone help me?

Thanks in advance.

Here my code
the view where my list is

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="FntrAudit.Views.RequiredDisplayPage"
             Title="Affichage Obligatoire">
    <Grid RowDefinitions="Auto,*,Auto"
       ColumnDefinitions="*,*,*">
        <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Text="Affichages obligatoires" FontSize="Large"></Label>

        <ListView Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding RequiredDisplays}">
            <ListView.ItemTemplate>
                <DataTemplate >
                    <ViewCell>
                        <StackLayout Orientation="Horizontal" VerticalOptions="Center" Spacing="10">
                            <CheckBox IsChecked="{Binding IsOk}"  VerticalOptions="Center"/>
                            <Label Text="{Binding Intitule}" VerticalOptions="Center" HorizontalOptions="FillAndExpand" LineBreakMode="WordWrap"/>
                            <ImageButton Clicked="UpdateDoc" HeightRequest="20" WidthRequest="20" Source="update.png" CommandParameter="{Binding .}"></ImageButton>
                            <ImageButton Clicked="Delete_Clicked" HeightRequest="20" WidthRequest="20" Source="delete.png" CommandParameter="{Binding .}"></ImageButton>
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Button   BackgroundColor="#AE1439" Grid.Row="2" Grid.Column="1"  Text="Enregistrer et revenir à l'audit" Margin="5" Clicked="Record_Clicked" HorizontalOptions="Fill"></Button>
        <Button   BackgroundColor="#AE1439" Grid.Row="2" Grid.Column="0"  Text="Ajouter document obligatoire" Margin="5" Clicked="AddDoc" HorizontalOptions="Fill"></Button>
    </Grid>
</ContentPage>

le code behind

using FntrAudit.DALApi;
using FntrAudit.Data;
using FntrAudit.Models;
using FntrAudit.Utils;
using FntrAudit.Views.Modal;
using Microsoft.EntityFrameworkCore;
using System.Collections.ObjectModel;

namespace FntrAudit.Views;

public partial class RequiredDisplayPage : ContentPage
{
    private Audit _audit;
    SqliteDbContext _db = new SqliteDbContext();
    private ObservableCollection<RequiredDisplay> requiredDisplays;
    private RestService<RequiredDisplay> _requiredDisplayService = new RestService<RequiredDisplay>();
    private RestService<LogFromApp> _LogService = new RestService<LogFromApp>();
    public ObservableCollection<RequiredDisplay> RequiredDisplays
    {
        get { return requiredDisplays; }
        set
        {
            if (requiredDisplays != value)
            {
                requiredDisplays = value;
                OnPropertyChanged(nameof(RequiredDisplays));
            }
        }
    }
    public RequiredDisplayPage(Audit audit)
    {
        InitializeComponent();
        _audit = audit;
        InitializeAsync();
    }
    private async void InitializeAsync()
    {
        await LoadRequiredDoc();
    }

    public async Task LoadRequiredDoc()
    {
        try
        {
            Console.WriteLine("Loading required documents...");
            bool hasWeb = Connectivity.Current.NetworkAccess == NetworkAccess.Internet;
            List<RequiredDisplay> requiredDisplays = new List<RequiredDisplay>();


            if (_audit.RequiredDisplay.Any(rd => rd.IsOk))
            {
                RequiredDisplays = new ObservableCollection<RequiredDisplay>(_audit.RequiredDisplay);
            }
            else
            {
                RequiredDisplays = new ObservableCollection<RequiredDisplay>(_audit.RequiredDisplay);

            }

            await Task.Delay(1000);
            this.BindingContext = this;
            Console.WriteLine("Required documents loaded successfully.");
        }
        catch(Exception ex)
        {
            var t = ex.Message;
            Console.WriteLine($"Error loading required documents: {ex.Message}");
        }
      
    }

    private async void Delete_Clicked(object sender, EventArgs e)
    {
        try
        {
            bool answer = await DisplayAlert("Supprimer un document obligatoire", "Êtes-vous sûr de vouloir supprimer ce document ?", "Oui", "Non");

            if (answer)
            {
                if (sender is ImageButton button && button.CommandParameter is RequiredDisplay toDelete)
                {
                    try
                    {
                        Console.WriteLine($"Attempting to delete document: {toDelete.GuidRDisplay}");
                        _db.RequiredDisplay.Remove(toDelete);
                        await _db.SaveChangesAsync();
                        await _requiredDisplayService.DeleteTodoItemAsync(toDelete.GuidRDisplay, Constantes.URLAPI + "RequiredDisplay/");
                        RequiredDisplays.Remove(toDelete);
                        Console.WriteLine($"Document {toDelete.GuidRDisplay} deleted successfully.");
                    }
                    catch (DbUpdateConcurrencyException)
                    {
                        await _db.Entry(toDelete).ReloadAsync();
                        await DisplayAlert("Échec de la suppression", "Le document a été modifié ou supprimé par une autre transaction. Veuillez réessayer.", "OK");
                        Console.WriteLine($"Concurrency error while deleting document: {toDelete.GuidRDisplay}");
                    }
                    catch (Exception ex)
                    {
                        LogFromApp logFromApp = new LogFromApp()
                        {
                            DateCreation = DateTime.Now,
                            Intitule = "ModalRequired delete doc (UserContext ContextCurrent) " + ex.Message,
                            Trace = ex.ToString(),
                            UserInvolved = " "
                        };

                        await _LogService.Create(logFromApp, Constantes.URLAPILOG, true);
                        await DisplayAlert("Erreur", "Une erreur est survenue lors de la suppression du document.", "OK");
                        Console.WriteLine($"Error deleting document: {ex.Message}");
                    }
                }
            }
        }
        catch (Exception ex)
        {
            await DisplayAlert("Erreur", "Une erreur est survenue lors de la suppression du document.", "OK");
            Console.WriteLine("Erreur lors de la suppression du document : " + ex.Message);
        }
    }

    //public async void Delete_Clicked(object sender, EventArgs e)
    //{
    //    bool answer = false;
    //    try
    //    {
    //         answer = await DisplayAlert("Supprimer un document obligatoire", "Êtes-vous sûr de vouloir supprimer ce document ?", "Oui", "Non");
    //    }
    //    catch(Exception ex)
    //    {
    //        var toto = ex.Message;
    //    }

    //    if (answer)
    //    {
    //        if (sender is ImageButton button && button.CommandParameter is RequiredDisplay toDelete)
    //        {
    //            try
    //            {
    //                _db.RequiredDisplay.Remove(toDelete);
    //                _db.SaveChanges();
    //                await _requiredDisplayService.DeleteTodoItemAsync(toDelete.GuidRDisplay, Constantes.URLAPI + "RequiredDisplay/");
    //                RequiredDisplays.Remove(toDelete);
    //            }
    //            catch (DbUpdateConcurrencyException ex)
    //            {
    //                // Option 1: Raffraîchir l'entité depuis la base de données
    //                await _db.Entry(toDelete).ReloadAsync();

    //                // Afficher un message d'erreur à l'utilisateur
    //                await DisplayAlert("Échec de la suppression", "Le document a été modifié ou supprimé par une autre transaction. Veuillez réessayer.", "OK");

    //                // Option 2: Tentative de réexécution de la suppression après rechargement
    //                // _db.RequiredDisplay.Remove(toDelete);
    //                // _db.SaveChanges();
    //            }
    //            catch (Exception ex)
    //            {
    //                // Afficher un message d'erreur générique
    //                await DisplayAlert("Erreur", "Une erreur est survenue lors de la suppression du document.", "OK");
    //            }
    //        }
    //    }
    //}
    public async void Record_Clicked(object sender, EventArgs e)
    {
        try
        {
            Console.WriteLine("Recording clicked.");
            _audit.RequiredDisplay = RequiredDisplays.ToList();
            await Application.Current.MainPage.Navigation.PopModalAsync();
            Console.WriteLine("Modal closed successfully.");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error in Record_Clicked: {ex.Message}");
        }
    }
    public async void AddDoc(object sender, EventArgs e)
    {
        try
        {
            RequiredDisplay requiredDisplay = new RequiredDisplay();
            var modal = new ModalRequiredDisplay(requiredDisplay, true);
            modal.RequiredDisplays = this.RequiredDisplays;
            //  await Application.Current.MainPage.Navigation.PushModalAsync(modal);

            var tcs = new TaskCompletionSource<bool>();
            modal.Disappearing += async (s, args) =>
            {
                Console.WriteLine("Modal disappearing...");
                if (!tcs.Task.IsCompleted)
                {
                    tcs.SetResult(true);
                }

            };
            await Application.Current.MainPage.Navigation.PushModalAsync(modal);
            await tcs.Task;  // Wait for the modal to close
            await LoadRequiredDoc();
        }
        catch(Exception ex)
        {
            Console.WriteLine($"Error in AddDoc: {ex.Message}");
        }
       
       
    }

    public async void UpdateDoc(object sender, EventArgs e)
    {
        try
        {
            if (sender is ImageButton button && button.CommandParameter is RequiredDisplay toUpdate)
            {
                var modal = new ModalRequiredDisplay(toUpdate, false);
                modal.RequiredDisplays = this.RequiredDisplays;

                var tcs = new TaskCompletionSource<bool>();
                modal.Disappearing += (s, args) =>
                {
                    Console.WriteLine("Modal disappearing...");
                    if (!tcs.Task.IsCompleted)
                    {
                        tcs.SetResult(true);
                    }
                };

                await Application.Current.MainPage.Navigation.PushModalAsync(modal);
                Console.WriteLine("Modal pushed to stack.");
                await tcs.Task;  // Wait for the modal to close
                Console.WriteLine("Modal closed.");
                await LoadRequiredDoc();
            }
        
        }catch(Exception ex)
        {
            var t = ex.Message;
        }
    }
}

the modal

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="FntrAudit.Views.Modal.ModalRequiredDisplay"
             BackgroundColor="#00000000"
             Title="Affichage Obligatoire">
    <ContentView HorizontalOptions="Center" VerticalOptions="Center">
        <ScrollView>
            <Frame BackgroundColor="White" CornerRadius="20" Padding="20"
               HorizontalOptions="Center" VerticalOptions="Center"
               WidthRequest="700" HeightRequest="600">
           
            <VerticalStackLayout Spacing="15">
                <Label Text="{Binding TitleModal}" TextColor="Black" FontSize="24" FontAttributes="Bold" HorizontalOptions="Center"></Label>

                <Editor Text="{Binding Intitule}" Placeholder="Intitulé du document" AutoSize="TextChanges"></Editor>

                <VerticalStackLayout Spacing="10">
                    <Label Text="Effectif de l'entreprise concernée (si besoin)" VerticalOptions="CenterAndExpand" FontAttributes="Bold"/>
                    <VerticalStackLayout Spacing="5">
                        <Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto" Padding="5">
                            <Label Text="Toutes" Grid.Column="0" VerticalOptions="CenterAndExpand" Margin="0,0,10,0"/>
                            <RadioButton x:Name="All" GroupName="Effectif" IsChecked="True" Grid.Column="2" VerticalOptions="CenterAndExpand"/>
                        </Grid>
                        <Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto" Padding="5">
                            <Label Text="de 1 à 10" Grid.Column="0" VerticalOptions="CenterAndExpand" Margin="0,0,10,0"/>
                            <RadioButton x:Name="Has1SalOrMore" GroupName="Effectif" IsChecked="{Binding Has1SalOrMore}" Grid.Column="2" VerticalOptions="CenterAndExpand"/>
                        </Grid>
                        <Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto" Padding="5">
                            <Label Text="de 11 à 50" Grid.Column="0" VerticalOptions="CenterAndExpand" Margin="0,0,10,0"/>
                            <RadioButton x:Name="Has11SalOrMore" GroupName="Effectif" IsChecked="{Binding Has11SalOrMore}" Grid.Column="2" VerticalOptions="CenterAndExpand"/>
                        </Grid>
                        <Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto" Padding="5">
                            <Label Text="de 51 à 300" Grid.Column="0" VerticalOptions="CenterAndExpand" Margin="0,0,10,0"/>
                            <RadioButton x:Name="Has50SalOrMore" GroupName="Effectif" IsChecked="{Binding Has50SalOrMore}" Grid.Column="2" VerticalOptions="CenterAndExpand"/>
                        </Grid>
                        <Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto" Padding="5">
                            <Label Text="de 301 à 1000" Grid.Column="0" VerticalOptions="CenterAndExpand" Margin="0,0,10,0"/>
                            <RadioButton x:Name="Has300SalOrMore" GroupName="Effectif" IsChecked="{Binding Has300SalOrMore}" Grid.Column="2" VerticalOptions="CenterAndExpand"/>
                        </Grid>
                        <Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto" Padding="5">
                            <Label Text="1000 et plus" Grid.Column="0" VerticalOptions="CenterAndExpand" Margin="0,0,10,0"/>
                            <RadioButton x:Name="Has1000SalOrMore" GroupName="Effectif" IsChecked="{Binding Has1000SalOrMore}" Grid.Column="2" VerticalOptions="CenterAndExpand"/>
                        </Grid>
                    </VerticalStackLayout>
                </VerticalStackLayout>

                <HorizontalStackLayout Spacing="10">
                    <Button BackgroundColor="#007AFF" TextColor="White" Text="Enregistrer" Margin="5" Clicked="AddDoc_Clicked" HorizontalOptions="FillAndExpand"></Button>
                    <Button BackgroundColor="#FF3B30" TextColor="White" Text="Quitter" Margin="5" Clicked="Quit_Clicked" HorizontalOptions="FillAndExpand"></Button>
                </HorizontalStackLayout>
            </VerticalStackLayout>
        </Frame>
        </ScrollView>
    </ContentView>
</ContentPage>

the code behind

using CommunityToolkit.Maui.Alerts;
using FntrAudit.DALApi;
using FntrAudit.Data;
using FntrAudit.Models;
using FntrAudit.Services;
using FntrAudit.Utils;
using Microsoft.EntityFrameworkCore;
using System.Collections.ObjectModel;

namespace FntrAudit.Views.Modal;

public partial class ModalRequiredDisplay : ContentPage
{
    private RequiredDisplay _document;
    private bool _isCreation;
    SqliteDbContext _db = new SqliteDbContext();
    private ObservableCollection<RequiredDisplay> requiredDisplays;
    private RestService<RequiredDisplay> _requiredDisplayService = new RestService<RequiredDisplay>();
    private RestService<LogFromApp> _LogService = new RestService<LogFromApp>();
    
    public ObservableCollection<RequiredDisplay> RequiredDisplays
    {
        get { return requiredDisplays; }
        set
        {
            if (requiredDisplays != value)
            {
                requiredDisplays = value;
                OnPropertyChanged(nameof(RequiredDisplays));

            }
        }
    }
    public ModalRequiredDisplay(RequiredDisplay document, bool isCreation)
    {
        InitializeComponent();
        _document = document;
        _isCreation = isCreation;
        
        _document.TitleModal = _isCreation ? "Ajout d'un document obligatoire" : "Mise à jour d'un document obligatoire";
       
        this.BindingContext = _document;     
    }

    private async void Delete_Clicked(object sender, EventArgs e)
    {
        await CloseModalAsync();
    }

    private async void Quit_Clicked(object sender, EventArgs e)
    {
        await CloseModalAsync();
    }

   

    private async Task AddDoc(object sender, EventArgs e)
    {
        bool hasWeb = Connectivity.Current.NetworkAccess == NetworkAccess.Internet;
       
        try
        {
            if (_isCreation)
            {

                RequiredDisplay required = new RequiredDisplay();
                _document.IsOk = false;
                _document.AuditId = 1;
                _document.GuidRDisplay = Guid.NewGuid().ToString();

                await _db.RequiredDisplay.AddAsync(_document);
                await _db.SaveChangesAsync();

                Func<Task> saveDocument = async () =>
                {
                    await _requiredDisplayService.Create(_document, Constantes.URLAPI + "RequiredDisplay", true);
                };

                if (hasWeb)
                {
                    await saveDocument.Invoke();
                }
                else
                {
                    var connectivityService = new ConnectivityService();
                    connectivityService.AddTask(saveDocument);
                }
            }
            else
            {
                _db.Update(_document);
                await _db.SaveChangesAsync();

                Func<Task> updateDocument = async () =>
                {
                    await _requiredDisplayService.Create(_document, Constantes.URLAPI + "RequiredDisplay/RequiredDisplay/" + _document.GuidRDisplay, false);
                };

                if (hasWeb)
                {
                    await updateDocument.Invoke();
                }
                else
                {
                    var connectivityService = new ConnectivityService();
                    connectivityService.AddTask(updateDocument);
                }

            }
           
            await ShowToasterAsync("Document obligatoire enregistré avec succès.");
        }
        catch (Exception ex)
        {
            LogFromApp logFromApp = new LogFromApp()
            {
                DateCreation = DateTime.Now,
                Intitule = "ModalRequired (UserContext ContextCurrent) " + ex.Message,
                Trace = ex.ToString(),
                UserInvolved = " "
            };

            await _LogService.Create(logFromApp, Constantes.URLAPILOG, true);
        }
        finally
        {
            try
            {
                if (RequiredDisplays == null)
                {
                    RequiredDisplays = new ObservableCollection<RequiredDisplay>();
                }

                RequiredDisplay requiredDisplay = null;

                if (RequiredDisplays.Count > 0 && _document != null)
                {
                    requiredDisplay = RequiredDisplays.FirstOrDefault(rd => rd.GuidRDisplay == _document.GuidRDisplay);
                }

                if (requiredDisplay != null)
                {
                    int index = RequiredDisplays.IndexOf(requiredDisplay);
                    if (index != -1)
                    {
                        RequiredDisplays[index] = _document;
                    }
                }

                await CloseModalAsync();
            }
            catch (Exception closeEx)
            {
                Console.WriteLine("Erreur lors de la fermeture de la modal : " + closeEx.Message);
            }

        }
    }

    private async Task CloseModalAsync()
    {
        try
        {
            if (Application.Current.MainPage.Navigation.ModalStack.Count > 0)
            {
                await Application.Current.MainPage.Navigation.PopModalAsync();
                Console.WriteLine("Modal closed successfully.");
            }
            else
            {
                Console.WriteLine("Modal stack is empty.");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error in CloseModalAsync: {ex.Message}");
        }
    }


    private async void DeleteDoc(object sender, EventArgs e)
    {
        bool answer = await DisplayAlert("Supprimer un document obligatoire", "Êtes-vous sûr de vouloir supprimer ce document ?", "Oui", "Non");

        if (answer)
        {
            if (sender is ImageButton button && button.CommandParameter is RequiredDisplay toDelete)
            {
                try
                {
                    _db.RequiredDisplay.Remove(toDelete);
                    await _db.SaveChangesAsync();
                    await _requiredDisplayService.DeleteTodoItemAsync(toDelete.GuidRDisplay, Constantes.URLAPI + "RequiredDisplay/");
                    RequiredDisplays.Remove(toDelete);
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    // Option 1: Raffraîchir l'entité depuis la base de données
                    await _db.Entry(toDelete).ReloadAsync();

                    // Afficher un message d'erreur à l'utilisateur
                    await DisplayAlert("Échec de la suppression", "Le document a été modifié ou supprimé par une autre transaction. Veuillez réessayer.", "OK");

                    // Option 2: Tentative de réexécution de la suppression après rechargement
                    // _db.RequiredDisplay.Remove(toDelete);
                    // _db.SaveChanges();
                }
                catch (Exception ex)
                {
                    LogFromApp logFromApp = new LogFromApp()
                    {
                        DateCreation = DateTime.Now,
                        Intitule = "ModalRequired delete doc (UserContext ContextCurrent) " + ex.Message,
                        Trace = ex.ToString(),
                        UserInvolved = " "
                    };

                    await _LogService.Create(logFromApp, Constantes.URLAPILOG, true);
                    // Afficher un message d'erreur générique
                    await DisplayAlert("Erreur", "Une erreur est survenue lors de la suppression du document.", "OK");
                }
            }
        }
    }
    private async Task ShowToasterAsync(string message)
    {
        var toast = Toast.Make(message);
        await toast.Show();
    }

    public async void AddDoc_Clicked(object sender, EventArgs e)
    {
        await AddDoc(sender, e);
    }
   
}

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật