Aplikacje mobilne · Zdarzenia

Zdarzenia kontrolek: Switch, Slider, Stepper, Picker i pickery dat

Każda kontrolka wyboru ma własne zdarzenie i własny typ EventArgs z konkretnymi danymi. Przejdziesz przez zdarzenia Switch, Slider, Stepper, Picker, DatePicker, TimePicker i SearchBar oraz zobaczysz, skąd wziąć nową wartość w każdym z nich.

  • około 50 minut
  • .NET MAUI · Toggled · ValueChanged · DateSelected
  • INF.04.6.2

Po tej lekcji: wskazać zdarzenie i typ EventArgs dla każdej kontrolki wyboru, odczytać nową wartość (e.Value, e.NewValue, e.NewDate) i trzymać stan między zdarzeniami w polu klasy.

Panel sterowany zdarzeniami kontrolek

Nie każda zmiana wymaga przycisku Zapisz. Suwak może od razu zmienić tekst, a przełącznik stan ekranu. Zobaczysz, jakie dane niesie samo zdarzenie.

Najpierw uruchomisz mały ekran. Potem połączysz go z kodem i sprawdzisz konkretne wyniki. Dodatki wykonuj dopiero, gdy wersja podstawowa działa. Quiz i oceniane zadania pozostają w biurku po zalogowaniu.

Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji

1 · Sprawdź w działaniu

Co zgłasza każda kontrolka

Dotknij każdej kontrolki i przeczytaj dziennik: nazwę zdarzenia i dane, które przekazuje. Zwróć uwagę, które zdarzenia mają e.NewValue, a które wymagają odczytu z kontrolki.

Najpierw przewidź, potem sprawdź: Zmień stan jednej kontrolki i odszukaj nowe dane zdarzenia. Tym razem interesuje nas e.Value lub e.NewValue, a nie sam wygląd kontrolki jak w lekcji o ankiecie.

Ta interakcja wymaga włączonego JavaScriptu.

Nowa wartość jest w e (np. e.Value, e.NewValue, e.NewDate) albo w samej kontrolce (SelectedItem, Text). Model pomaga zobaczyć mechanizm; właściwą aplikację uruchomisz z plików poniżej.

1. Przygotuj miejsce pracy

Co dodajesz, a co podmieniasz?

Stronę dodaj przez Projekt → Dodaj → Nowy element → .NET MAUI ContentPage (XAML). Powstaje para .xaml i .xaml.cs. Dla ContentView wybierz szablon ContentView; dla modeli i serwisów zwykłą klasę. Jeśli plik już istnieje, podmień wskazaną zawartość — nie twórz drugiej klasy o tej samej nazwie.

PlikRola w ćwiczeniu
ZdarzeniaKontrolekPage.xamlWygląd strony lub kontrolki
ZdarzeniaKontrolekPage.xaml.csLogika i obsługa danych
LogowaniePage.xamlWygląd strony lub kontrolki
LogowaniePage.xaml.csLogika i obsługa danych

Zanim uruchomisz: trzy połączenia

x:Class="MojaAplikacja.NazwaStrony" w XAML musi zgadzać się z namespace MojaAplikacja i nazwą klasy C#. InitializeComponent() tworzy kontrolki opisane w XAML.

x:Name="PoleImie" daje kodowi C# dostęp przez PoleImie.Text. Z kolei Clicked="Zapisz_Clicked" wskazuje metodę o tej nazwie w pliku .xaml.cs. Sam napis przycisku w Text niczego nie podpina.

Gdy dodajesz XAML z obsługą zdarzeń, od razu uzupełnij też odpowiadający C#. Uruchamiaj dopiero po skończeniu oznaczonej pary plików; brakujący handler nie jest błędem środowiska.

2. Uruchom sam wygląd — jeszcze bez logiki

W świeżym projekcie zastąp MainPage.xaml poniższym ekranem. W MainPage.xaml.cs zachowaj konstruktor z InitializeComponent(); usuń szablonowy licznik i OnCounterClicked, bo nie ma ich już w XAML. Na tym etapie App i AppShell pozostają z szablonu. To pełny, tymczasowy XAML — później świadomie go podmienisz, nie dokleisz do drugiego ContentPage.

MainPage.xaml — etap tymczasowyxaml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MojaAplikacja.MainPage">
    <VerticalStackLayout Padding="24" Spacing="14">
        <Label Text="Panel sterowania" FontSize="24" />
        <Switch />
        <Slider Minimum="0" Maximum="100" Value="50" />
    </VerticalStackLayout>
</ContentPage>
  1. Pierwsza próba Uruchom. Zobaczysz kontrolki, ale przyciski bez Clicked jeszcze nic nie robią. Sprawdź rozmiar i kolejność elementów. Zatrzymaj aplikację przed następnym etapem.

Zacznijmy od egzaminu

Konkretne nazwy zdarzeń to częste pytanie

W zadaniach egzaminacyjnych trzeba reagować na konkretną kontrolkę: przełącznik, suwak, datę. Znajomość nazw zdarzeń i typów EventArgs (Toggled, ValueChanged, DateSelected…) jest regularnie sprawdzana.

Kluczowa wskazówka: w EventArgs zwykle jest nowa wartość, a gdy jej tam nie ma (np. Picker, SearchBar), odczytujesz ją z samej kontrolki.

2 · Ściągawka

Zdarzenia i typy EventArgs

KontrolkaZdarzenieTyp `EventArgs` i dane
SwitchToggledToggledEventArgs: e.Value (bool), nowy stan.
SliderValueChangedValueChangedEventArgs: e.NewValue (double), także e.OldValue.
StepperValueChangedValueChangedEventArgs: e.NewValue (double).
DatePickerDateSelectedDateChangedEventArgs: e.NewDate (DateTime? w MAUI10). Sprawdź brak wybranej daty.
TimePickerTimeSelected (MAUI10)TimeChangedEventArgs: e.NewTime (TimeSpan?). W starszych projektach spotkasz obserwację PropertyChanged.
PickerSelectedIndexChangedZwykłe EventArgs: wartość z SelectedItem lub SelectedIndex.
SearchBarSearchButtonPressedZwykłe EventArgs: wartość z Text kontrolki (lub z sender).
W starszych materiałach przeczytasz, że `TimePicker` nie ma własnego zdarzenia i trzeba użyć `PropertyChanged`. W aktualnej wersji MAUI jest `TimeSelected`, ale na egzaminie możesz spotkać obie wersje.

3. Wskaż stronę startową ćwiczenia

Po dodaniu docelowych stron podmień App.xaml.cs na poniższy. App.xaml zostaje z szablonu. Od tej chwili startuje ZdarzeniaKontrolekPage, a nie wcześniejszy tymczasowy MainPage z AppShell. Uzupełnij jej pliki w kolejnych etapach, zanim ponownie uruchomisz projekt.

App.xaml.cscsharp
namespace MojaAplikacja;

public partial class App : Application
{
    public App() => InitializeComponent();
    protected override Window CreateWindow(IActivationState? activationState)
        => new Window(new NavigationPage(new ZdarzeniaKontrolekPage()));
}
  1. CreateWindow Tworzy okno aplikacji. NavigationPage jest kontenerem zwykłych stron i daje przycisk powrotu; nie łączymy go z Shell.
  2. new ZdarzeniaKontrolekPage() Tworzy instancję docelowej strony. Aby testować drugi niezależny przykład, zatrzymaj program, zmień tylko ten typ i uruchom ponownie.

4. Połącz wygląd z działaniem

Teraz zbuduj docelowy przykład. Pełne pliki poniżej zastępują swoje wcześniejsze wersje. Przeczytaj opis nad kodem i objaśnienia pod nim; potem sprawdź przepływ danych, nie tylko brak czerwonych podkreśleń.

Gdy przykład ma kilka plików zależnych od siebie, najpierw dodaj wszystkie wskazane typy, a potem uruchom całość. Ustaw breakpoint w obsłudze głównego przycisku i prześledź jedną próbę.

Budowa krok po kroku

ZdarzeniaKontrolekPage.xaml

Dodaj ZdarzeniaKontrolekPage. Przy każdej kontrolce znajdź zdarzenie i nazwę handlera. Zapis x:Name nie zastępuje podłączenia zdarzenia.

Każda kontrolka ma w XAML własny atrybut zdarzenia. Nazwy metod muszą być identyczne z C#.

ZdarzeniaKontrolekPage.xamlxaml
<?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="MojaAplikacja.ZdarzeniaKontrolekPage">
    <ScrollView>
        <VerticalStackLayout Padding="20" Spacing="12">
            <Switch x:Name="TrybNocny" Toggled="TrybNocny_Toggled" />

            <Label x:Name="EtykietaJasnosc" Text="Jasność: 50" />
            <Slider x:Name="JasnoscSlider" Minimum="0" Maximum="100" Value="50"
                    ValueChanged="JasnoscSlider_ValueChanged" />

            <Label x:Name="EtykietaWiek" Text="Wiek: 18" />
            <Stepper x:Name="WiekStepper" Minimum="13" Maximum="99" Value="18"
                     ValueChanged="WiekStepper_ValueChanged" />

            <DatePicker x:Name="DataUrodzenia" DateSelected="DataUrodzenia_DateSelected" />
            <TimePicker x:Name="GodzinaKontaktu" TimeSelected="GodzinaKontaktu_TimeSelected" />

            <Picker x:Name="PytaniePomocnicze" Title="Pytanie pomocnicze"
                    SelectedIndexChanged="Pytanie_SelectedIndexChanged">
                <Picker.ItemsSource>
                    <x:Array Type="{x:Type x:String}">
                        <x:String>Imię pierwszego zwierzaka</x:String>
                        <x:String>Miasto urodzenia</x:String>
                        <x:String>Ulubiona książka</x:String>
                    </x:Array>
                </Picker.ItemsSource>
            </Picker>

            <SearchBar x:Name="SzukajMiasta" Placeholder="Szukaj miasta"
                       SearchButtonPressed="SzukajMiasta_SearchButtonPressed" />

            <Label x:Name="EtykietaWynik" />
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>
  1. Toggled="...", ValueChanged="..." Atrybuty zdarzeń Switch, Slider i Stepper.
  2. DateSelected, TimeSelected Zdarzenia pickerów daty i godziny.
  3. SelectedIndexChanged, SearchButtonPressed Zdarzenia Picker i SearchBar.

Budowa krok po kroku

ZdarzeniaKontrolekPage.xaml.cs

Porównuj handler z jego kontrolką: Toggled daje bool, ValueChanged nową liczbę, a Picker wymaga odczytu SelectedItem. SearchButtonPressed uruchamia wyszukanie dopiero po zatwierdzeniu.

Zwróć uwagę na typ drugiego parametru: dla każdej kontrolki jest inny.

ZdarzeniaKontrolekPage.xaml.cscsharp
namespace MojaAplikacja;

public partial class ZdarzeniaKontrolekPage : ContentPage
{
    private readonly string[] _miasta = { "Kraków", "Warszawa", "Gdańsk", "Poznań", "Wrocław" };

    public ZdarzeniaKontrolekPage()
    {
        InitializeComponent();
    }

    // Switch: e.Value to nowy stan (bool)
    private void TrybNocny_Toggled(object sender, ToggledEventArgs e)
    {
        BackgroundColor = e.Value ? Colors.Black : Colors.White;
    }

    // Slider: e.NewValue to double
    private void JasnoscSlider_ValueChanged(object sender, ValueChangedEventArgs e)
    {
        EtykietaJasnosc.Text = $"Jasność: {(int)e.NewValue}";
    }

    // Stepper: też ValueChangedEventArgs
    private void WiekStepper_ValueChanged(object sender, ValueChangedEventArgs e)
    {
        EtykietaWiek.Text = $"Wiek: {e.NewValue:F0}";
    }

    // DatePicker: e.NewDate to DateTime
    private void DataUrodzenia_DateSelected(object sender, DateChangedEventArgs e)
    {
        EtykietaWynik.Text = $"Data: {e.NewDate:dd.MM.yyyy}";
    }

    // TimePicker (nowsze wersje MAUI): e.NewTime to TimeSpan
    private void GodzinaKontaktu_TimeSelected(object sender, TimeChangedEventArgs e)
    {
        EtykietaWynik.Text = $"Godzina: {e.NewTime:hh\\:mm}";
    }

    // Picker: zwykłe EventArgs, wartość czytamy z kontrolki
    private void Pytanie_SelectedIndexChanged(object sender, EventArgs e)
    {
        if (PytaniePomocnicze.SelectedIndex >= 0)
            EtykietaWynik.Text = $"Pytanie: {PytaniePomocnicze.SelectedItem}";
    }

    // SearchBar: tekst czytamy z kontrolki
    private void SzukajMiasta_SearchButtonPressed(object sender, EventArgs e)
    {
        string fraza = SzukajMiasta.Text ?? "";
        bool jest = _miasta.Any(m => m.Equals(fraza, StringComparison.OrdinalIgnoreCase));
        EtykietaWynik.Text = jest ? $"{fraza}: jest na liście" : $"{fraza}: brak na liście";
    }
}
  1. e.Value ? Colors.Black : Colors.White e.Value to nowy stan przełącznika. Trzeba go odczytać z e, bo kontrolka mogła się już zmienić.
  2. (int)e.NewValue i {e.NewValue:F0} NewValue to double. Rzutowanie albo format F0 zamieniają go na liczbę bez miejsc po przecinku.
  3. e.NewDate:dd.MM.yyyy Format daty zapisany w interpolacji.
  4. e.NewTime:hh\\:mm Godzina z TimeSpan. Dwukropek w formacie trzeba poprzedzić ukośnikiem.
  5. PytaniePomocnicze.SelectedItem W Picker wartość czytasz z kontrolki, bo EventArgs nic nie niesie.
  6. _miasta.Any(m => m.Equals(fraza, OrdinalIgnoreCase)) Sprawdza, czy miasto jest na liście, bez rozróżniania wielkości liter.

Zatrzymaj się przed kolejnym wariantem

Nie zastępuj jeszcze działającej wersji. Wykonaj tę próbę i dopiero potem przejdź do dodatku.

Dopiero teraz: drugi wariant lub rozszerzenie

To następny etap tej lekcji, nie warunek pierwszego uruchomienia. Porównaj jego rolę z działającą bazą. Jeśli brakuje czasu, wróć do niego po sprawdzeniu podstawowej wersji.

Dodatek do działającej wersji

LogowaniePage.xaml

Osobny wariant LogowaniePage łączy kliknięcie z wcześniej zmienioną flagą przełącznika. Nie dodawaj go jako drugiego korzenia poprzedniej strony.

Przełącznik „Zapamiętaj mnie” ustawia stan, który potem zużyje przycisk „Zaloguj”.

LogowaniePage.xamlxaml
<?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="MojaAplikacja.LogowaniePage" Title="Logowanie">
    <VerticalStackLayout Padding="20" Spacing="12">
        <Entry x:Name="PoleLogin" Placeholder="Login" />
        <Entry x:Name="PoleHaslo" Placeholder="Hasło" IsPassword="True" />

        <HorizontalStackLayout Spacing="10">
            <Switch x:Name="PrzelacznikZapamietaj" Toggled="Zapamietaj_Toggled" />
            <Label Text="Zapamiętaj mnie" VerticalOptions="Center" />
        </HorizontalStackLayout>

        <Button Text="Zaloguj" Clicked="Zaloguj_Click" />
        <Label x:Name="StatusLabel" />
    </VerticalStackLayout>
</ContentPage>
  1. Przeczytaj powiązanie x:Name identyfikuje kontrolkę, a Binding odczytuje właściwość obiektu ustawionego jako BindingContext. To dwa różne mechanizmy.

Dodatek do działającej wersji

LogowaniePage.xaml.cs

admin/1234 to jawne dane ćwiczeniowe. _zapamietajZaznaczone istnieje tylko w pamięci strony; nie przechowuje hasła i nie tworzy sesji.

To lokalna demonstracja zdarzeń na fikcyjnych danych admin/1234, nie bezpieczne logowanie. Switch zmienia wyłącznie flagę w pamięci; nie zapisuje sesji ani hasła. Nie używaj prawdziwych danych logowania.

LogowaniePage.xaml.cscsharp
namespace MojaAplikacja;

public partial class LogowaniePage : ContentPage
{
    private bool _zapamietajZaznaczone = false;

    public LogowaniePage() => InitializeComponent();

    private void Zapamietaj_Toggled(object sender, ToggledEventArgs e)
    {
        // e.Value to aktualny stan przełącznika (true/false)
        _zapamietajZaznaczone = e.Value;
    }

    private async void Zaloguj_Click(object sender, EventArgs e)
    {
        string? login = PoleLogin.Text?.Trim();
        string? haslo = PoleHaslo.Text;

        if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(haslo))
        {
            await DisplayAlertAsync("Błąd", "Uzupełnij login i hasło.", "OK");
            return;
        }

        // Prosta „logika” logowania: w prawdziwej aplikacji tu byłby
        // dostęp do bazy danych albo zapytanie do serwera (REST API)
        bool ok = (login == "admin" && haslo == "1234");

        StatusLabel.Text = ok
            ? $"Zalogowano{(_zapamietajZaznaczone ? " (zaznaczono opcję)" : "")}"
            : "Nieprawidłowe dane";
    }
}
  1. private bool _zapamietajZaznaczone Pole klasy: widoczne dla wszystkich metod i żyje tak długo, jak strona.
  2. _zapamietajZaznaczone = e.Value Handler Toggled zapisuje nowy stan.
  3. bool ok = (login == "admin" && haslo == "1234") Prosta „logika” logowania. W prawdziwej aplikacji byłaby baza lub zapytanie do serwera.
  4. $"Zalogowano{(… ? " (zapamiętano)" : "")}" Operator warunkowy wewnątrz interpolacji, w nawiasach.

5. Odbiór: czy naprawdę działa?

Wykonaj próby w swojej aplikacji, nie tylko w modelu na stronie. Zaznaczaj dopiero po sprawdzeniu wyniku. Funkcje urządzenia wymagają testu na właściwej platformie.

7 · Czego unikać

Cztery błędy z zdarzeniami kontrolek

Odczyt `e.NewValue` jako `int` bez rzutowania
Tak nie
int poziom = e.NewValue;

Błąd kompilatora: double nie konwertuje się niejawnie na int.

Tak
int poziom = (int)e.NewValue;   // albo Math.Round(e.NewValue)

Slider i Stepper przekazują double.

Stan w zmiennej lokalnej zamiast w polu klasy
Tak nie
private void Zapamietaj_Toggled(object sender, ToggledEventArgs e)
{
    bool zapamietaj = e.Value;   // zniknie po wyjściu z metody
}

Przycisk „Zaloguj” nie widzi stanu przełącznika.

Tak
private bool _zapamietaj;

private void Zapamietaj_Toggled(object sender, ToggledEventArgs e)
{
    _zapamietaj = e.Value;
}

Dane współdzielone między handlerami trzymaj w polu klasy.

Nazwa właściwości wpisana na sztywno
Tak nie
if (e.PropertyName == "Time")

Działa, ale literówka ("Tiem") nie zostanie wykryta przez kompilator.

Tak
if (e.PropertyName == nameof(TimePicker.Time))

nameof jest sprawdzane w czasie kompilacji.

Odczyt `SelectedItem` bez sprawdzenia
Tak nie
EtykietaWynik.Text = PytaniePomocnicze.SelectedItem.ToString();

Gdy nic nie wybrano, SelectedItem jest null i metoda zgłasza wyjątek.

Tak
if (PytaniePomocnicze.SelectedIndex >= 0)
    EtykietaWynik.Text = $"Pytanie: {PytaniePomocnicze.SelectedItem}";

Sprawdź wybór, zanim użyjesz wartości.

Ćwiczenie z pomocą · samodzielna praca

6. Mała zmiana, którą zrobisz samodzielnie

Dodaj etykietę opisującą poziom suwaka: niski lub wysoki.

Oczekiwany rezultat
Dodaj etykietę opisującą poziom suwaka: niski lub wysoki.
Dotychczasowe działanie ma pozostać poprawne.
Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź

W istniejącym ValueChanged odczytaj e.NewValue.

Krok 2 — pokaż podpowiedź

Porównaj wartość z wybranym progiem i ustaw Text nowego Label.

Krok 3 — pokaż podpowiedź

Sprawdź wartości poniżej progu, równą progowi i powyżej.

Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.

8 · Sprawdź się

Które zdarzenie i skąd wartość?

Cztery sytuacje.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Switch: Toggled i e.Value. Slider, Stepper: ValueChanged i e.NewValue (double). DatePicker: DateSelected i e.NewDate.
  2. TimePicker: TimeSelected (nowsze) albo PropertyChanged z nameof(TimePicker.Time).
  3. Picker i SearchBar: zwykłe EventArgs, wartość czytasz z kontrolki.
  4. double na int wymaga rzutowania.
  5. Stan współdzielony przez kilka handlerów trzymaj w polu klasy.

Dalej na swoim koncie

Rozgrzewki, quiz i zadania są w biurku ucznia.

Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.

Zaloguj się →
Źródła i dalsza nauka