Aplikacja test – wczytywanie pytań z json

MainPage.xaml.cs

using Microsoft.Maui.Controls.Shapes;
using TestAplikacjeMobilne.Logika;
using TestAplikacjeMobilne.Modele;

namespace TestAplikacjeMobilne;

public partial class MainPage : ContentPage
{
    private const string NazwaPlikuJson = "pytania_inf04_maui.json";
    private const int IloscLosowanychPytan = 15;

    private List<ModelePytanieTestowe> pytania = new();
    private readonly Dictionary<int, int?> zaznaczenia = new(); // klucz: index pytania, wartosc: index odpowiedzi

    public MainPage()
    {
        InitializeComponent();
    }

    protected override async void OnAppearing()
    {
        base.OnAppearing();

        // Żeby nie dublować po przełączaniu w menu
        if (pytania.Count > 0) return;

        var baza = await LogikaTestu.WczytajBazePytanAsync(NazwaPlikuJson);
        LabelOpis.Text = baza.NazwaTestu;

        pytania = LogikaTestu.LosujPytania(baza.Pytania, IloscLosowanychPytan);

        WyswietlPytania();
    }

    private void WyswietlPytania()
    {
        KontenerPytan.Children.Clear();
        zaznaczenia.Clear();

        for (int i = 0; i < pytania.Count; i++)
        {
            zaznaczenia[i] = null;

            var p = pytania[i];

            // Ramka (Border)
            var border = new Border
            {
                Stroke = Colors.Gray,
                StrokeThickness = 1,
                Padding = 12,
                StrokeShape = new RoundRectangle { CornerRadius = 10 }
            };

            var stack = new VerticalStackLayout { Spacing = 8 };

            stack.Children.Add(new Label
            {
                Text = $"Pytanie {i + 1}",
                FontAttributes = FontAttributes.Bold
            });

            stack.Children.Add(new Label
            {
                Text = p.Tresc,
                FontSize = 15
            });

            string groupName = $"P{i}";

            for (int j = 0; j < p.Odpowiedzi.Count; j++)
            {
                int indexPytania = i;
                int indexOdp = j;

                var rb = new RadioButton
                {
                    Content = p.Odpowiedzi[j],
                    GroupName = groupName
                };

                rb.CheckedChanged += (s, e) =>
                {
                    if (e.Value == true)
                        zaznaczenia[indexPytania] = indexOdp;
                };

                stack.Children.Add(rb);
            }

            border.Content = stack;
            KontenerPytan.Children.Add(border);
        }
    }

    private async void Sprawdz_Clicked(object sender, EventArgs e)
    {
        int punkty = 0;

        for (int i = 0; i < pytania.Count; i++)
        {
            var zazn = zaznaczenia[i];
            if (zazn.HasValue && zazn.Value == pytania[i].Poprawna)
                punkty++;
        }

        await Shell.Current.GoToAsync(nameof(PodsumowaniePage),
            new Dictionary<string, object>
            {
                { "Punkty", punkty },
                { "Maks", pytania.Count }
            });
    }
}
MainPage.xaml

<?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="TestAplikacjeMobilne.MainPage"
    Title="TEST – MAUI">

    <ScrollView>
        <VerticalStackLayout Padding="20" Spacing="14">

            <Label Text="TEST (losowanie 15 pytań)"
                   FontSize="24"
                   FontAttributes="Bold"
                   HorizontalOptions="Center"/>

            <Label x:Name="LabelOpis"
                   Text=""
                   FontSize="14"
                   HorizontalOptions="Center"/>

            <!-- tutaj wypełnimy pytania w kodzie -->
            <VerticalStackLayout x:Name="KontenerPytan" Spacing="12" />

            <Button Text="SPRAWDŹ"
                    FontSize="18"
                    Clicked="Sprawdz_Clicked" />

        </VerticalStackLayout>
    </ScrollView>
</ContentPage>
Logika/LogikaTestu.cs

using System.Text.Json;
using TestAplikacjeMobilne.Modele;

namespace TestAplikacjeMobilne.Logika;

public static class LogikaTestu
{
    public static async Task<ModeleBazaPytan> WczytajBazePytanAsync(string nazwaPliku)
    {
        using var stream = await FileSystem.OpenAppPackageFileAsync(nazwaPliku);
        using var reader = new StreamReader(stream);
        string json = await reader.ReadToEndAsync();

        var baza = JsonSerializer.Deserialize<ModeleBazaPytan>(json, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });

        return baza ?? new ModeleBazaPytan();
    }

    public static List<ModelePytanieTestowe> LosujPytania(List<ModelePytanieTestowe> wszystkie, int ile)
    {
        // proste i czytelne losowanie
        return wszystkie
            .OrderBy(x => Guid.NewGuid())
            .Take(ile)
            .ToList();
    }
}
Modele/ModelePytanieTestowe.cs

namespace TestAplikacjeMobilne.Modele;

public class ModelePytanieTestowe
{
    public int Id { get; set; }
    public string Tresc { get; set; } = "";
    public List<string> Odpowiedzi { get; set; } = new();
    public int Poprawna { get; set; }  // indeks 0..3
}

public class ModeleBazaPytan
{
    public string NazwaTestu { get; set; } = "";
    public List<ModelePytanieTestowe> Pytania { get; set; } = new();
}
Resources/Raw/pytania_inf04_maui.json

{
  "nazwaTestu": "INF.04 – .NET MAUI (losowanie 15/40)",
  "pytania": [
    {
      "id": 1,
      "tresc": "Który plik najczęściej zawiera układ interfejsu w .NET MAUI?",
      "odpowiedzi": [ "Program.cs", "MainPage.xaml", "AppShell.xaml.cs", "launchSettings.json" ],
      "poprawna": 1
    },
    {
      "id": 2,
      "tresc": "Jak nazywa się zdarzenie przycisku Button wywoływane po kliknięciu?",
      "odpowiedzi": [ "Changed", "Clicked", "PressedKey", "Load" ],
      "poprawna": 1
    },
    {
      "id": 3,
      "tresc": "Do czego służy Entry w MAUI?",
      "odpowiedzi": [ "Do wprowadzania tekstu", "Do wyświetlania obrazu", "Do listy elementów", "Do nawigacji" ],
      "poprawna": 0
    },
    {
      "id": 4,
      "tresc": "Który układ (layout) układa elementy jeden pod drugim?",
      "odpowiedzi": [ "VerticalStackLayout", "Grid", "AbsoluteLayout", "FlexLayout (zawsze)" ],
      "poprawna": 0
    },
    {
      "id": 5,
      "tresc": "Jak w MAUI odczytać plik dołączony jako asset (Resources/Raw)?",
      "odpowiedzi": [ "File.ReadAllText(\"...\")", "FileSystem.OpenAppPackageFileAsync(\"...\")", "HttpClient.GetStringAsync(\"...\")", "Directory.GetFiles(\"...\")" ],
      "poprawna": 1
    },
    {
      "id": 6,
      "tresc": "Który element w AppShell odpowiada za pozycję w menu bocznym (flyout)?",
      "odpowiedzi": [ "FlyoutItem", "Label", "Entry", "BoxView" ],
      "poprawna": 0
    },
    {
      "id": 7,
      "tresc": "Jak nazywa się nowoczesna nawigacja w Shell?",
      "odpowiedzi": [ "Navigation.PushAsync()", "Shell.Current.GoToAsync()", "Window.Navigate()", "Page.Open()" ],
      "poprawna": 1
    },
    {
      "id": 8,
      "tresc": "Co zwraca metoda int.TryParse(...) gdy konwersja się nie uda?",
      "odpowiedzi": [ "Rzuca wyjątek", "Zwraca false", "Zwraca -1", "Zwraca null" ],
      "poprawna": 1
    },
    {
      "id": 9,
      "tresc": "Który element najlepiej nadaje się do wyboru jednej opcji z kilku (jak w teście)?",
      "odpowiedzi": [ "RadioButton", "Label", "Image", "ProgressBar" ],
      "poprawna": 0
    },
    {
      "id": 10,
      "tresc": "Do czego służy IsPassword w Entry?",
      "odpowiedzi": [ "Zamienia Entry w label", "Ukrywa wpisywane znaki", "Dodaje walidację e-mail", "Ustawia limit znaków" ],
      "poprawna": 1
    },
    {
      "id": 11,
      "tresc": "Która kolekcja jest często używana do list w UI, bo powiadamia o zmianach?",
      "odpowiedzi": [ "List<T>", "ObservableCollection<T>", "Stack<T>", "Dictionary<T,T>" ],
      "poprawna": 1
    },
    {
      "id": 12,
      "tresc": "Który element w MAUI jest odpowiednikiem „pola tekstowego” wprowadzania danych?",
      "odpowiedzi": [ "Entry", "Label", "Image", "Frame" ],
      "poprawna": 0
    },
    {
      "id": 13,
      "tresc": "Jaką właściwością w Label ustawisz pogrubienie tekstu?",
      "odpowiedzi": [ "FontAttributes", "TextDecorations", "BoldText", "Weight" ],
      "poprawna": 0
    },
    {
      "id": 14,
      "tresc": "Do czego służy Grid w MAUI?",
      "odpowiedzi": [ "Do układania wierszy i kolumn", "Tylko do obrazków", "Tylko do przycisków", "Do bazy danych" ],
      "poprawna": 0
    },
    {
      "id": 15,
      "tresc": "Który zapis tworzy metodę obsługi zdarzenia Clicked?",
      "odpowiedzi": [ "void Clicked()", "private void Button_Clicked(object s, EventArgs e)", "int Clicked(string x)", "async Task OnLoad()" ],
      "poprawna": 1
    },
    {
      "id": 16,
      "tresc": "Po co stosuje się async/await w aplikacji mobilnej?",
      "odpowiedzi": [ "Żeby UI nie „zamrażało” się", "Żeby XAML się kompilował", "Żeby przyciski działały", "Żeby zmienić kolor tła" ],
      "poprawna": 0
    },
    {
      "id": 17,
      "tresc": "Która kontrolka pokazuje krótki tekst na ekranie?",
      "odpowiedzi": [ "Label", "Entry", "Slider", "Switch" ],
      "poprawna": 0
    },
    {
      "id": 18,
      "tresc": "Jak w MAUI wyświetlić szybkie okno z komunikatem (dialog)?",
      "odpowiedzi": [ "MessageBox.Show(...)", "DisplayAlert(...)", "Console.WriteLine(...)", "Toast.Show(...) (zawsze)" ],
      "poprawna": 1
    },
    {
      "id": 19,
      "tresc": "Co jest prawdą o RadioButtonach w jednej grupie?",
      "odpowiedzi": [ "Można zaznaczyć wszystkie", "Można zaznaczyć tylko jeden", "Nie da się odznaczyć", "Nie mają GroupName" ],
      "poprawna": 1
    },
    {
      "id": 20,
      "tresc": "Który plik zwykle zawiera ustawienie: MainPage = new AppShell(); ?",
      "odpowiedzi": [ "App.xaml.cs", "Program.cs", "MainPage.xaml", "MauiProgram.cs" ],
      "poprawna": 0
    },
    {
      "id": 21,
      "tresc": "Jak nazywa się plik, w którym rejestrujesz usługi DI w MAUI?",
      "odpowiedzi": [ "MauiProgram.cs", "AppShell.xaml", "MainPage.xaml.cs", "Resources.xaml" ],
      "poprawna": 0
    },
    {
      "id": 22,
      "tresc": "Która właściwość kontroluje widoczność elementu w MAUI?",
      "odpowiedzi": [ "IsVisible", "Visible", "Show", "Opacity (zawsze)" ],
      "poprawna": 0
    },
    {
      "id": 23,
      "tresc": "Jak nazywa się kontrolka przełącznika (tak/nie)?",
      "odpowiedzi": [ "Switch", "Toggle", "Check", "Flip" ],
      "poprawna": 0
    },
    {
      "id": 24,
      "tresc": "Która kontrolka służy do wyboru z listy rozwijanej?",
      "odpowiedzi": [ "Picker", "Slider", "ImageButton", "BoxView" ],
      "poprawna": 0
    },
    {
      "id": 25,
      "tresc": "Co robi FileSystem.OpenAppPackageFileAsync(...) w MAUI?",
      "odpowiedzi": [ "Otwiera plik z internetu", "Otwiera plik dołączony do aplikacji", "Tworzy nowy plik na dysku", "Kasuje plik" ],
      "poprawna": 1
    },
    {
      "id": 26,
      "tresc": "Jak w MAUI ustawić margines wokół kontrolki?",
      "odpowiedzi": [ "Margin", "Padding (zawsze)", "Border", "Spacing" ],
      "poprawna": 0
    },
    {
      "id": 27,
      "tresc": "Która właściwość w Button ustawia tekst na przycisku?",
      "odpowiedzi": [ "Text", "Title", "Caption", "Value" ],
      "poprawna": 0
    },
    {
      "id": 28,
      "tresc": "Jak w Shell cofnąć się do poprzedniej strony?",
      "odpowiedzi": [ "Shell.Current.GoToAsync(\"..\");", "Navigation.PopAsync()", "App.Back()", "Window.Close()" ],
      "poprawna": 0
    },
    {
      "id": 29,
      "tresc": "Co oznacza, że metoda jest 'async'?",
      "odpowiedzi": [ "Może zawierać await", "Zawsze zwraca int", "Nie może mieć parametrów", "Nigdy nie kończy pracy" ],
      "poprawna": 0
    },
    {
      "id": 30,
      "tresc": "Która kontrolka pokazuje obraz w MAUI?",
      "odpowiedzi": [ "Image", "Label", "Border", "Entry" ],
      "poprawna": 0
    },
    {
      "id": 31,
      "tresc": "Gdzie najczęściej trzymasz kolory, style i zasoby aplikacji?",
      "odpowiedzi": [ "App.xaml", "MainPage.xaml.cs", "Program.cs", "launchSettings.json" ],
      "poprawna": 0
    },
    {
      "id": 32,
      "tresc": "Co jest lepsze do wielu ekranów z menu: Shell czy NavigationPage?",
      "odpowiedzi": [ "Shell", "NavigationPage", "Oba zawsze identyczne", "Żadne" ],
      "poprawna": 0
    },
    {
      "id": 33,
      "tresc": "Który wyjątek często występuje przy złej konwersji string→int, jeśli nie użyjesz TryParse?",
      "odpowiedzi": [ "FormatException", "NullReferenceException", "IndexOutOfRangeException", "DivideByZeroException" ],
      "poprawna": 0
    },
    {
      "id": 34,
      "tresc": "Jaką właściwością ustawisz odstęp między elementami w VerticalStackLayout?",
      "odpowiedzi": [ "Spacing", "Margin", "Padding", "Gap" ],
      "poprawna": 0
    },
    {
      "id": 35,
      "tresc": "Co jest prawdą o JSON?",
      "odpowiedzi": [ "To format danych tekstowych", "To baza danych", "To język programowania", "To plik EXE" ],
      "poprawna": 0
    },
    {
      "id": 36,
      "tresc": "Jak w C# wczytać JSON do obiektów (najprościej)?",
      "odpowiedzi": [ "System.Text.Json.JsonSerializer.Deserialize", "Console.ReadKey", "Directory.ReadAllText", "Bitmap.Load" ],
      "poprawna": 0
    },
    {
      "id": 37,
      "tresc": "Który layout najłatwiej daje dwie kolumny (np. etykieta + pole)?",
      "odpowiedzi": [ "Grid", "VerticalStackLayout", "ScrollView", "Border" ],
      "poprawna": 0
    },
    {
      "id": 38,
      "tresc": "Co robi Title w ContentPage?",
      "odpowiedzi": [ "Ustawia tytuł strony w pasku", "Ustawia kolor tła", "Ustawia rozmiar okna", "Ustawia czcionkę globalnie" ],
      "poprawna": 0
    },
    {
      "id": 39,
      "tresc": "Dlaczego w aplikacji mobilnej nie robi się pętli while do obsługi pytań w UI?",
      "odpowiedzi": [ "Bo zablokuje interfejs", "Bo while nie działa w C#", "Bo nie ma zmiennych", "Bo JSON tego zabrania" ],
      "poprawna": 0
    },
    {
      "id": 40,
      "tresc": "Która instrukcja losuje elementy listy w C# (najprościej) przez sortowanie po Guid?",
      "odpowiedzi": [ "OrderBy(x => Guid.NewGuid())", "SortAscending()", "Randomize()", "ShuffleAll()" ],
      "poprawna": 0
    }
  ]
}
App.xaml






App.xaml.cs

using Microsoft.Extensions.DependencyInjection;

namespace TestAplikacjeMobilne
{
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            MainPage = new AppShell();   // <- MUSI BYĆ AppShell
        }
    }

}
AppShell.xaml.cs

namespace TestAplikacjeMobilne;

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();

        Routing.RegisterRoute(nameof(PodsumowaniePage), typeof(PodsumowaniePage));
    }
}



AppShell.xaml 

<?xml version="1.0" encoding="utf-8" ?>
<Shell
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:TestAplikacjeMobilne"
    x:Class="TestAplikacjeMobilne.AppShell"
    FlyoutBehavior="Flyout">

    <FlyoutItem Title="Test App Mob">
        <ShellContent
            Title="Test App Mob"
            ContentTemplate="{DataTemplate local:MainPage}" />
    </FlyoutItem>

</Shell>