Aplikacje mobilne · Projekt z kilku elementów
Zegar i terminarz w .NET MAUI
Zegar i kalendarz to klasyczne przykłady prostych aplikacji mobilnych z podstawy programowej INF.04.6. Połączysz kontrolki czasu, zdarzenia, model danych i ObservableCollection w jeden projekt: zegar odświeżany co sekundę i formularz wydarzeń z walidacją terminu.
- około 75 minut
- .NET MAUI · Dispatcher · DatePicker · TimePicker
- INF.04.6.2
Po tej lekcji: uruchomić cykliczny timer, złożyć termin z DatePicker.Date i TimePicker.Time, zwalidować go oraz utrzymać listę posortowaną chronologicznie.
Zegar i lista nadchodzących wydarzeń
Data, godzina i odświeżanie ekranu to trzy osobne sprawy. Złożymy termin wydarzenia i uruchomimy jeden timer, który pracuje tylko, gdy strona jest widoczna.
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
Zegar, walidacja i sortowanie
Dodaj trzy wydarzenia w losowej kolejności: lista ustawi je chronologicznie. Spróbuj dodać termin z przeszłości, puste pole nazwy oraz włącz oba przełączniki błędów po prawej: timer zwracający false i pominięcie godziny.
Najpierw przewidź, potem sprawdź: W modelu terminarza ustaw dwa różne terminy i porównaj kolejność. Rozróżnij zegar od wywołania systemowego alarmu — to nie ta sama funkcja.
Zegar żyje dzięki return true w funkcji timera. Pełny termin to data plus godzina. Porządek na liście to efekt sortowania przy każdym dodaniu. 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.
| Plik | Rola w ćwiczeniu |
|---|---|
| Modele/Wydarzenie.cs | Logika i obsługa danych |
| MainPage.xaml | Wygląd strony lub kontrolki |
| MainPage.xaml.cs | Logika i obsługa danych |
| Fragment C# | Fragment w istniejącym pliku |
Zanim uruchomisz: trzy połączenia
x:Class="TerminarzMauiApp.NazwaStrony" w XAML musi zgadzać się z namespace TerminarzMauiApp 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.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TerminarzMauiApp.MainPage">
<VerticalStackLayout Padding="24" Spacing="14">
<Label Text="Mój terminarz" FontSize="24" />
<Entry Placeholder="Nazwa wydarzenia" />
<DatePicker />
<TimePicker />
</VerticalStackLayout>
</ContentPage>
- 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.
Aplikacja przypominająca o terminach
Zegar u góry, najbliższe wydarzenie na górze listy
Terminarz wizyt lekarskich, oddawania projektów czy urodzin działa tak samo: zegar pokazuje, która jest teraz, a lista nadchodzących wydarzeń jest uporządkowana tak, by najbliższy termin zawsze był na górze.
To projekt podsumowujący: łączy rzeczy, które już znasz, w jedną całość.
2 · Cztery techniki
Z czego zbudowany jest terminarz
| Technika | Zastosowanie |
|---|---|
| Dispatcher.CreateTimer() / IDispatcherTimer | Tworzy timer z Tick, Interval, Start i Stop. Przy opuszczaniu strony zatrzymaj go. |
| data.Date + czas | Najpierw odczytaj niepuste DateTime i TimeSpan z pickerów nullable, potem połącz datę i godzinę. |
termin < DateTime.Now | Walidacja: nie pozwalamy dodać wydarzenia z przeszłości. |
OrderBy + Clear + Add | Sortowanie ObservableCollection przez posortowaną kopię. |
TerminOpis => Termin.ToString(...) | Właściwość wyliczana: formatowanie w jednym miejscu. |
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 MainPage, a nie wcześniejszy tymczasowy MainPage z AppShell. Uzupełnij jej pliki w kolejnych etapach, zanim ponownie uruchomisz projekt.
namespace TerminarzMauiApp;
public partial class App : Application
{
public App() => InitializeComponent();
protected override Window CreateWindow(IActivationState? activationState)
=> new Window(new NavigationPage(new MainPage()));
}
CreateWindowTworzy okno aplikacji. NavigationPage jest kontenerem zwykłych stron i daje przycisk powrotu; nie łączymy go z Shell.new MainPage()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
Modele/Wydarzenie.cs
Wydarzenie ma nazwę i jeden połączony termin. TerminOpis jest obliczany z daty, nie przechowywany jako drugi niezależny termin.
Dwie zwykłe właściwości i jedna wyliczana bez set.
namespace TerminarzMauiApp.Modele;
public class Wydarzenie
{
public string Nazwa { get; set; } = "";
public DateTime Termin { get; set; }
public string TerminOpis => Termin.ToString("dd.MM.yyyy HH:mm");
}
TerminOpis => Termin.ToString("dd.MM.yyyy HH:mm")Czytelny tekst zDateTimebez ręcznego formatowania w każdym miejscu.
Budowa krok po kroku
MainPage.xaml
Dodaj lub otwórz MainPage.xaml i podmień jego zawartość. Najpierw odszukaj kontener, potem nazwane kontrolki oraz wskazane zdarzenia. Pozostaw zgodne x:Class i nazwę klasy w pliku .xaml.cs.
Siatka z czterema wierszami: zegar, formularz, przycisk i lista (* zajmuje resztę miejsca).
<?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"
xmlns:modele="clr-namespace:TerminarzMauiApp.Modele"
x:Class="TerminarzMauiApp.MainPage"
Title="Zegar i terminarz">
<Grid Padding="20" RowDefinitions="Auto,Auto,Auto,Auto,*" RowSpacing="14">
<Label x:Name="ZegarLabel" FontSize="36" FontAttributes="Bold" HorizontalOptions="Center" />
<VerticalStackLayout Grid.Row="1" Spacing="8">
<Entry x:Name="NazwaEntry" Placeholder="Nazwa wydarzenia" />
<DatePicker x:Name="DataPicker" />
<TimePicker x:Name="CzasPicker" />
</VerticalStackLayout>
<Button Grid.Row="2" Text="Dodaj wydarzenie" Clicked="Dodaj_Clicked" />
<Button Text="Sprawdź opuszczenie strony" Clicked="OtworzProbe_Clicked" Grid.Row="3" />
<CollectionView Grid.Row="4" x:Name="ListaWydarzen" EmptyView="Brak wydarzeń.">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="modele:Wydarzenie">
<Grid Padding="8" ColumnDefinitions="*,Auto">
<Label Text="{Binding Nazwa}" />
<Label Grid.Column="1" Text="{Binding TerminOpis}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
RowDefinitions="Auto,Auto,Auto,*"Trzy wiersze o wysokości zawartości i jeden rozciągnięty na resztę ekranu.x:DataType="modele:Wydarzenie"Informuje kompilator, jaki typ ma szablon, więc wiązania są kompilowane (szybsze i sprawdzane). Bez tego kompilator zgłasza ostrzeżenia.EmptyView="Brak wydarzeń."Tekst pokazywany, gdy lista jest pusta.
Budowa krok po kroku
MainPage.xaml.cs
Konstruktor tworzy timer raz. OnAppearing go uruchamia, OnDisappearing zatrzymuje. Handler dodawania najpierw sprawdza dane, a potem sortuje kolekcję.
Dispatcher.CreateTimer daje timer, który można jawnie zatrzymać. Startujemy go przy pokazaniu strony i zatrzymujemy przy jej opuszczeniu. DatePicker.Date i TimePicker.Time w MAUI10 mogą być null — najpierw odczytujemy konkretne wartości.
using System.Collections.ObjectModel;
using TerminarzMauiApp.Modele;
namespace TerminarzMauiApp;
public partial class MainPage : ContentPage
{
private readonly ObservableCollection<Wydarzenie> _wydarzenia = new();
public MainPage()
{
InitializeComponent();
ListaWydarzen.ItemsSource = _wydarzenia;
_timer = Dispatcher.CreateTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (_, _) => ZegarLabel.Text = DateTime.Now.ToString("HH:mm:ss");
}
private readonly Microsoft.Maui.Dispatching.IDispatcherTimer _timer;
protected override void OnAppearing()
{
base.OnAppearing();
ZegarLabel.Text = DateTime.Now.ToString("HH:mm:ss");
_timer.Start();
}
protected override void OnDisappearing()
{
_timer.Stop();
base.OnDisappearing();
}
private async void Dodaj_Clicked(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(NazwaEntry.Text))
{
await DisplayAlertAsync("Brak nazwy", "Wpisz nazwę wydarzenia.", "OK");
return;
}
if (DataPicker.Date is not DateTime data || CzasPicker.Time is not TimeSpan czas)
{
await DisplayAlertAsync("Termin", "Wybierz datę i godzinę.", "OK");
return;
}
DateTime termin = data.Date + czas;
if (termin < DateTime.Now)
{
await DisplayAlertAsync("Błędny termin", "Wybierz datę i godzinę z przyszłości.", "OK");
return;
}
_wydarzenia.Add(new Wydarzenie { Nazwa = NazwaEntry.Text.Trim(), Termin = termin });
var uporzadkowane = _wydarzenia.OrderBy(x => x.Termin).ToList();
_wydarzenia.Clear();
foreach (Wydarzenie wydarzenie in uporzadkowane)
_wydarzenia.Add(wydarzenie);
NazwaEntry.Text = "";
}
private async void OtworzProbe_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new ContentPage
{
Title = "Ekran próbny",
Content = new Label { Text = "Wróć strzałką wstecz.", Margin = 24 }
});
}
}
_timer.Tick += ...Zdarzenie każdego tyknięcia odświeża Label. Znak += dopina obsługę; nie tworzymy nowego timera przy każdym powrocie.is not DateTime data/is not TimeSpan czasSprawdzenie wartości nullable i przypisanie niepustej wartości do zmiennej. Brak wyboru kończy handler.data.Date + czasŁączy samą datę z godziną; nie dodajemy drugiej bieżącej daty.
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
Gdy spotkasz krótszy Dispatcher.StartTimer
To osobna alternatywa do analizy, nie dopisek do działającego zegara. return false oznacza koniec powtórzeń. Nie uruchamiaj drugiego timera obok _timer.
To alternatywny skrócony zapis. Nie doklejaj go do zegara z tej lekcji — uruchomiłbyś drugi mechanizm. Dla jawnego Start/Stop użyliśmy CreateTimer.
Dispatcher.StartTimer(TimeSpan.FromSeconds(1), () =>
{
System.Diagnostics.Debug.WriteLine("Tyknięcie");
return false; // false kończy kolejne wywołania, true kontynuuje
});
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.
6 · Czego unikać
Trzy błędy w terminarzu
Timer zwraca false
Dispatcher.StartTimer(TimeSpan.FromSeconds(1), () =>
{
ZegarLabel.Text = DateTime.Now.ToString("HH:mm:ss");
return false;
});Zegar zaktualizuje się dokładnie raz i stanie.
return true;true oznacza „wywołaj mnie ponownie”.
Sama data bez godziny
DateTime termin = DataPicker.Date;Data ma godzinę 00:00, więc wydarzenie „dzisiaj o 15:00” wypadnie jako przeszłe.
DateTime termin = DataPicker.Date + CzasPicker.Time;Pełny termin to data plus godzina.
Brak walidacji
_wydarzenia.Add(new Wydarzenie { Nazwa = NazwaEntry.Text, Termin = termin });Puste nazwy i daty z przeszłości psują użyteczność listy.
if (string.IsNullOrWhiteSpace(NazwaEntry.Text)) { ... return; }
if (termin < DateTime.Now) { ... return; }Sprawdź dane, zanim trafią do kolekcji.
Ćwiczenie z pomocą · samodzielna praca
6. Mała zmiana, którą zrobisz samodzielnie
Pokaż liczbę wydarzeń na liście.
Pokaż liczbę wydarzeń na liście. Dotychczasowe działanie ma pozostać poprawne.
? Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź
Dodaj nazwaną etykietę pod nagłówkiem.
Krok 2 — pokaż podpowiedź
Po dodaniu i sortowaniu odczytaj _wydarzenia.Count.
Krok 3 — pokaż podpowiedź
Nie zwiększaj licznika przed walidacją, bo błędne zgłoszenie nie tworzy wydarzenia.
Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.
7 · Sprawdź się
Dlaczego tak?
Cztery pytania.
Podsumowanie
Co warto zapamiętać
- IDispatcherTimer: Start uruchamia zegar, Stop go zatrzymuje. Stwórz timer raz i nie dokładaj kolejnego przy każdym powrocie.
- Sprawdź nullable Date/Time, potem połącz konkretną datę i godzinę.
- Waliduj nazwę i termin, zanim dodasz wydarzenie.
- Sortuj
ObservableCollectionprzez posortowaną kopię:OrderBy,Clear,Add. x:DataTypew szablonie włącza kompilowane wiązania.
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ę →