בניית אפליקציית .NET MAUI מקצה לקצה: MVVM, הזרקת תלויות וניווט Shell
אם אתם מפתחי .NET שרוצים לבנות אפליקציה חוצת פלטפורמות, סביר להניח שכבר שמעתם על .NET MAUI. אז בואו נודה באמת — הימים שבהם כתבנו אפליקציה נפרדת לכל פלטפורמה כבר מאחורינו. .NET MAUI 10 הגיע עם שיפורי ביצועים רציניים, Hot Reload משופר, רכיבי UI חדשים ואינטגרציה הדוקה יותר עם Native APIs. במדריך הזה נבנה ביחד אפליקציית ניהול משימות שלמה — מהמודלים ועד ה-UI — תוך שימוש בדפוסי הארכיטקטורה שבאמת חשובים: MVVM, הזרקת תלויות ו-Shell Navigation.
למה דווקא MAUI ולא Flutter או React Native? כי ב-MAUI אנחנו נשארים בתוך עולם ה-.NET המוכר. גישה מלאה לספריות NuGet, ביצועים גבוהים בזכות קומפילציית AOT, ותמיכה מובנית בדפוסי עיצוב מודרניים. גרסה 10 הוסיפה (בין היתר) CollectionView משופר עם גלילה חלקה יותר, Hybrid WebView מתקדם, וזמני בנייה שהתקצרו באופן מורגש.
הקמת הפרויקט
נתחיל מהבסיס. פתחו טרמינל וצרו פרויקט חדש:
dotnet new maui -n TaskMaster
cd TaskMaster
עכשיו, לפני שמתחילים לכתוב קוד, חשוב לארגן את מבנה התיקיות. מבנה טוב חוסך כאבי ראש בהמשך הדרך:
TaskMaster/
├── Models/
│ ├── TaskItem.cs
│ └── TaskCategory.cs
├── ViewModels/
│ ├── BaseViewModel.cs
│ ├── TaskListViewModel.cs
│ ├── TaskDetailViewModel.cs
│ └── AddTaskViewModel.cs
├── Views/
│ ├── TaskListPage.xaml
│ ├── TaskDetailPage.xaml
│ └── AddTaskPage.xaml
├── Services/
│ ├── ITaskService.cs
│ ├── TaskService.cs
│ ├── IDatabaseService.cs
│ └── DatabaseService.cs
├── Data/
│ └── AppDbContext.cs
├── Converters/
│ └── BoolToColorConverter.cs
├── App.xaml
├── AppShell.xaml
└── MauiProgram.cs
התקינו את חבילות ה-NuGet שנצטרך:
dotnet add package CommunityToolkit.Mvvm
dotnet add package sqlite-net-pcl
dotnet add package SQLitePCLRaw.bundle_green
dotnet add package CommunityToolkit.Maui
CommunityToolkit.Mvvm היא הספרייה הרשמית של מיקרוסופט למימוש MVVM בצורה נקייה. sqlite-net-pcl נותנת גישה פשוטה ומהירה ל-SQLite, ו-CommunityToolkit.Maui מוסיפה רכיבי UI שימושיים שתודו לי שהתקנתם אותם מוקדם.
ארכיטקטורת MVVM עם CommunityToolkit.Mvvm
אני אומר את זה בכנות — דפוס MVVM הוא לא רק "עוד דפוס ארכיטקטורה". הוא פשוט הכרחי כשמדובר בפיתוח MAUI. ההפרדה בין הלוגיקה העסקית לממשק המשתמש מאפשרת בדיקות יחידה קלות, תחזוקה נוחה ושימוש חוזר ברכיבים. והחלק הכי טוב? CommunityToolkit.Mvvm משתמשת ב-Source Generators שמייצרים את כל ה-boilerplate בשבילכם.
מודל הנתונים
נתחיל מהמודלים. הנה המודל של משימה בודדת:
// Models/TaskItem.cs
using SQLite;
namespace TaskMaster.Models;
public class TaskItem
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public bool IsCompleted { get; set; }
public DateTime DueDate { get; set; } = DateTime.Now;
public DateTime CreatedAt { get; set; } = DateTime.Now;
// עדיפות: 0=נמוכה, 1=בינונית, 2=גבוהה
public int Priority { get; set; }
public string Category { get; set; } = "כללי";
}
// Models/TaskCategory.cs
using SQLite;
namespace TaskMaster.Models;
public class TaskCategory
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string IconGlyph { get; set; } = string.Empty;
public string ColorHex { get; set; } = "#512BD4";
}
ObservableObject ו-ObservableProperty
ObservableObject הוא מחלקת הבסיס שמספקת את מנגנון ההתראות על שינויים (INotifyPropertyChanged). במקום לכתוב ידנית כל מאפיין עם אירוע PropertyChanged — דבר שמי שעשה את זה יודע כמה זה מייגע — פשוט משתמשים ב-[ObservableProperty] וזה מייצר הכל אוטומטית:
// ViewModels/TaskListViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using TaskMaster.Models;
using TaskMaster.Services;
namespace TaskMaster.ViewModels;
public partial class TaskListViewModel : ObservableObject
{
private readonly ITaskService _taskService;
[ObservableProperty]
private ObservableCollection<TaskItem> _tasks = new();
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _searchText = string.Empty;
[ObservableProperty]
private bool _isEmpty;
// כשטקסט החיפוש משתנה, מפעילים סינון מחדש
partial void OnSearchTextChanged(string value)
{
_ = FilterTasksAsync(value);
}
public TaskListViewModel(ITaskService taskService)
{
_taskService = taskService;
}
[RelayCommand]
private async Task LoadTasksAsync()
{
if (IsLoading) return;
try
{
IsLoading = true;
var tasks = await _taskService.GetAllTasksAsync();
Tasks = new ObservableCollection<TaskItem>(tasks);
IsEmpty = Tasks.Count == 0;
}
catch (Exception ex)
{
await Shell.Current.DisplayAlert(
"שגיאה",
$"לא ניתן לטעון משימות: {ex.Message}",
"אישור");
}
finally
{
IsLoading = false;
}
}
private async Task FilterTasksAsync(string searchText)
{
var allTasks = await _taskService.GetAllTasksAsync();
if (string.IsNullOrWhiteSpace(searchText))
{
Tasks = new ObservableCollection<TaskItem>(allTasks);
}
else
{
var filtered = allTasks
.Where(t => t.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase)
|| t.Description.Contains(searchText, StringComparison.OrdinalIgnoreCase))
.ToList();
Tasks = new ObservableCollection<TaskItem>(filtered);
}
IsEmpty = Tasks.Count == 0;
}
[RelayCommand]
private async Task DeleteTaskAsync(TaskItem task)
{
bool confirm = await Shell.Current.DisplayAlert(
"אישור מחיקה",
$"האם למחוק את המשימה '{task.Title}'?",
"מחק", "ביטול");
if (confirm)
{
await _taskService.DeleteTaskAsync(task.Id);
Tasks.Remove(task);
IsEmpty = Tasks.Count == 0;
}
}
[RelayCommand]
private async Task ToggleCompletedAsync(TaskItem task)
{
task.IsCompleted = !task.IsCompleted;
await _taskService.UpdateTaskAsync(task);
}
[RelayCommand]
private async Task GoToAddTaskAsync()
{
await Shell.Current.GoToAsync(nameof(Views.AddTaskPage));
}
[RelayCommand]
private async Task GoToTaskDetailAsync(TaskItem task)
{
await Shell.Current.GoToAsync(
$"{nameof(Views.TaskDetailPage)}?taskId={task.Id}");
}
}
RelayCommand ו-AsyncRelayCommand
שימו לב ל-attribute [RelayCommand] שמופיע מעל המתודות. הוא מייצר אוטומטית פקודות ICommand שאפשר לחבר ל-XAML. עבור מתודות אסינכרוניות, ה-Source Generator מייצר AsyncRelayCommand שמטפל במצבי טעינה וביטול. אפס קוד ICommand ידני. פשוט עובד.
הנה ה-ViewModel לדף פרטי המשימה, עם שימוש ב-IQueryAttributable לקבלת פרמטרים מהניווט:
// ViewModels/TaskDetailViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using TaskMaster.Models;
using TaskMaster.Services;
namespace TaskMaster.ViewModels;
public partial class TaskDetailViewModel : ObservableObject, IQueryAttributable
{
private readonly ITaskService _taskService;
[ObservableProperty]
private TaskItem _task = new();
[ObservableProperty]
private bool _isEditing;
[ObservableProperty]
private string _pageTitle = "פרטי משימה";
[ObservableProperty]
private string _editTitle = string.Empty;
[ObservableProperty]
private string _editDescription = string.Empty;
[ObservableProperty]
private DateTime _editDueDate = DateTime.Now;
[ObservableProperty]
private int _editPriority;
public TaskDetailViewModel(ITaskService taskService)
{
_taskService = taskService;
}
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("taskId", out var taskIdObj)
&& int.TryParse(taskIdObj?.ToString(), out int taskId))
{
_ = LoadTaskAsync(taskId);
}
}
private async Task LoadTaskAsync(int taskId)
{
var task = await _taskService.GetTaskByIdAsync(taskId);
if (task is not null)
{
Task = task;
PageTitle = task.Title;
EditTitle = task.Title;
EditDescription = task.Description;
EditDueDate = task.DueDate;
EditPriority = task.Priority;
}
}
[RelayCommand]
private void StartEditing()
{
IsEditing = true;
}
[RelayCommand]
private void CancelEditing()
{
EditTitle = Task.Title;
EditDescription = Task.Description;
EditDueDate = Task.DueDate;
EditPriority = Task.Priority;
IsEditing = false;
}
[RelayCommand]
private async Task SaveChangesAsync()
{
if (string.IsNullOrWhiteSpace(EditTitle))
{
await Shell.Current.DisplayAlert("שגיאה", "כותרת המשימה לא יכולה להיות ריקה", "אישור");
return;
}
Task.Title = EditTitle;
Task.Description = EditDescription;
Task.DueDate = EditDueDate;
Task.Priority = EditPriority;
await _taskService.UpdateTaskAsync(Task);
PageTitle = Task.Title;
IsEditing = false;
await Shell.Current.DisplayAlert("הצלחה", "המשימה עודכנה בהצלחה", "אישור");
}
[RelayCommand]
private async Task DeleteAndGoBackAsync()
{
bool confirm = await Shell.Current.DisplayAlert(
"אישור מחיקה",
"האם אתה בטוח שברצונך למחוק משימה זו?",
"מחק", "ביטול");
if (confirm)
{
await _taskService.DeleteTaskAsync(Task.Id);
await Shell.Current.GoToAsync("..");
}
}
[RelayCommand]
private async Task GoBackAsync()
{
await Shell.Current.GoToAsync("..");
}
}
ועכשיו ה-ViewModel להוספת משימה חדשה:
// ViewModels/AddTaskViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using TaskMaster.Models;
using TaskMaster.Services;
namespace TaskMaster.ViewModels;
public partial class AddTaskViewModel : ObservableObject
{
private readonly ITaskService _taskService;
[ObservableProperty]
private string _title = string.Empty;
[ObservableProperty]
private string _description = string.Empty;
[ObservableProperty]
private DateTime _dueDate = DateTime.Now.AddDays(1);
[ObservableProperty]
private int _selectedPriority;
[ObservableProperty]
private string _selectedCategory = "כללי";
public List<string> Categories { get; } = new()
{
"כללי", "עבודה", "אישי", "קניות", "בריאות", "לימודים"
};
public List<string> Priorities { get; } = new()
{
"נמוכה", "בינונית", "גבוהה"
};
public DateTime MinDate { get; } = DateTime.Now;
public AddTaskViewModel(ITaskService taskService)
{
_taskService = taskService;
}
[RelayCommand]
private async Task SaveTaskAsync()
{
if (string.IsNullOrWhiteSpace(Title))
{
await Shell.Current.DisplayAlert(
"שגיאה", "יש להזין כותרת למשימה", "אישור");
return;
}
var newTask = new TaskItem
{
Title = Title.Trim(),
Description = Description?.Trim() ?? string.Empty,
DueDate = DueDate,
Priority = SelectedPriority,
Category = SelectedCategory,
IsCompleted = false,
CreatedAt = DateTime.Now
};
await _taskService.AddTaskAsync(newTask);
await Shell.Current.GoToAsync("..");
}
[RelayCommand]
private async Task CancelAsync()
{
await Shell.Current.GoToAsync("..");
}
}
הזרקת תלויות (Dependency Injection)
אחד הדברים שאני הכי אוהב ב-.NET MAUI הוא התמיכה המובנית בהזרקת תלויות — אותה מערכת DI מוכרת מ-ASP.NET Core. אם עבדתם עם ASP.NET Core בעבר, תרגישו בבית. הרעיון פשוט: במקום שכל מחלקה תיצור את התלויות שלה בעצמה, אנחנו מגדירים מרכזית איך ליצור ולנהל את כל השירותים.
אורך חיי השירות (Service Lifetimes)
שלושה סוגי חיים, כל אחד עם המקום שלו:
- Singleton — מופע אחד יחיד לכל חיי האפליקציה. מושלם לשירותים כמו מסד נתונים או הגדרות שצריכים להיות זמינים תמיד.
- Transient — מופע חדש בכל פעם שמבקשים. מתאים ל-ViewModels שצריכים "להתחיל מאפס" בכל כניסה לדף.
- Scoped — מופע אחד לכל scope. ב-MAUI הוא מתנהג בדומה ל-Transient ברוב המקרים, אבל שימושי כשעובדים עם Navigation scope.
הגדרת השירותים ב-MauiProgram.cs
MauiProgram.cs הוא נקודת הכניסה של האפליקציה. כאן מגדירים הכל — שירותים, ViewModels ודפים:
// MauiProgram.cs
using CommunityToolkit.Maui;
using TaskMaster.Services;
using TaskMaster.ViewModels;
using TaskMaster.Views;
namespace TaskMaster;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
fonts.AddFont("FluentIcons.ttf", "FluentIcons");
});
// שירותים - Singleton כי צריכים מופע אחד
builder.Services.AddSingleton<IDatabaseService, DatabaseService>();
builder.Services.AddSingleton<ITaskService, TaskService>();
// ViewModels - Transient כי כל דף מקבל מופע חדש
builder.Services.AddTransient<TaskListViewModel>();
builder.Services.AddTransient<TaskDetailViewModel>();
builder.Services.AddTransient<AddTaskViewModel>();
// דפים - Transient כדי שייוצרו מחדש בכל ניווט
builder.Services.AddTransient<TaskListPage>();
builder.Services.AddTransient<TaskDetailPage>();
builder.Services.AddTransient<AddTaskPage>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
הגדרת שכבת השירותים
נגדיר ממשק ומימוש לשירות המשימות. שימוש בממשק מאפשר להחליף את המימוש בקלות — למשל, למוק בבדיקות (וזה שווה זהב כשמגיעים לכתוב Unit Tests):
// Services/ITaskService.cs
namespace TaskMaster.Services;
using TaskMaster.Models;
public interface ITaskService
{
Task<List<TaskItem>> GetAllTasksAsync();
Task<TaskItem?> GetTaskByIdAsync(int id);
Task<List<TaskItem>> GetTasksByCategoryAsync(string category);
Task<List<TaskItem>> GetPendingTasksAsync();
Task<int> AddTaskAsync(TaskItem task);
Task<int> UpdateTaskAsync(TaskItem task);
Task<int> DeleteTaskAsync(int id);
Task<int> GetTaskCountAsync();
}
// Services/TaskService.cs
namespace TaskMaster.Services;
using TaskMaster.Models;
public class TaskService : ITaskService
{
private readonly IDatabaseService _databaseService;
public TaskService(IDatabaseService databaseService)
{
_databaseService = databaseService;
}
public async Task<List<TaskItem>> GetAllTasksAsync()
{
var db = await _databaseService.GetConnectionAsync();
return await db.Table<TaskItem>()
.OrderByDescending(t => t.Priority)
.ThenBy(t => t.DueDate)
.ToListAsync();
}
public async Task<TaskItem?> GetTaskByIdAsync(int id)
{
var db = await _databaseService.GetConnectionAsync();
return await db.Table<TaskItem>()
.FirstOrDefaultAsync(t => t.Id == id);
}
public async Task<List<TaskItem>> GetTasksByCategoryAsync(string category)
{
var db = await _databaseService.GetConnectionAsync();
return await db.Table<TaskItem>()
.Where(t => t.Category == category)
.ToListAsync();
}
public async Task<List<TaskItem>> GetPendingTasksAsync()
{
var db = await _databaseService.GetConnectionAsync();
return await db.Table<TaskItem>()
.Where(t => !t.IsCompleted)
.OrderBy(t => t.DueDate)
.ToListAsync();
}
public async Task<int> AddTaskAsync(TaskItem task)
{
var db = await _databaseService.GetConnectionAsync();
return await db.InsertAsync(task);
}
public async Task<int> UpdateTaskAsync(TaskItem task)
{
var db = await _databaseService.GetConnectionAsync();
return await db.UpdateAsync(task);
}
public async Task<int> DeleteTaskAsync(int id)
{
var db = await _databaseService.GetConnectionAsync();
return await db.DeleteAsync<TaskItem>(id);
}
public async Task<int> GetTaskCountAsync()
{
var db = await _databaseService.GetConnectionAsync();
return await db.Table<TaskItem>().CountAsync();
}
}
ניווט Shell
Shell Navigation הוא מנגנון הניווט המומלץ ב-.NET MAUI, ולא בכדי. הוא נותן מבנה ניווט מבוסס URI, אנימציות מובנות, תפריט צד אוטומטי ותמיכה בניווט מבוסס נתיבים. פשוט עובד בצורה אינטואיטיבית — גם דרך XAML וגם דרך קוד.
הגדרת AppShell
AppShell.xaml מגדיר את מבנה הניווט הראשי:
<!-- AppShell.xaml -->
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="TaskMaster.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:TaskMaster.Views"
Title="מנהל המשימות"
Shell.FlyoutBehavior="Flyout"
FlyoutBackgroundColor="{StaticResource PrimaryColor}">
<FlyoutItem Title="המשימות שלי" Icon="tasks_icon.png">
<ShellContent
Title="כל המשימות"
ContentTemplate="{DataTemplate views:TaskListPage}"
Route="TaskList" />
</FlyoutItem>
<FlyoutItem Title="הוסף משימה" Icon="add_icon.png">
<ShellContent
Title="משימה חדשה"
ContentTemplate="{DataTemplate views:AddTaskPage}"
Route="AddTask" />
</FlyoutItem>
<Shell.FlyoutHeader>
<VerticalStackLayout
Padding="20"
BackgroundColor="{StaticResource PrimaryDarkColor}"
Spacing="10">
<Image
Source="app_logo.png"
HeightRequest="80"
WidthRequest="80" />
<Label
Text="מנהל המשימות"
FontSize="24"
TextColor="White"
FontAttributes="Bold"
HorizontalOptions="Center" />
<Label
Text="ניהול המשימות שלך בקלות"
FontSize="14"
TextColor="White"
Opacity="0.8"
HorizontalOptions="Center" />
</VerticalStackLayout>
</Shell.FlyoutHeader>
</Shell>
רישום נתיבים (Routes)
ב-Code-Behind של AppShell, נרשום נתיבים לדפים שלא מוגדרים ישירות ב-XAML:
// AppShell.xaml.cs
namespace TaskMaster;
using TaskMaster.Views;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// דפים שצריכים ניווט פרוגרמטי ולא מופיעים בתפריט
Routing.RegisterRoute(nameof(TaskDetailPage), typeof(TaskDetailPage));
Routing.RegisterRoute(nameof(AddTaskPage), typeof(AddTaskPage));
}
}
ניווט עם GoToAsync והעברת פרמטרים
ניווט ב-Shell מתבצע עם GoToAsync. יש כמה דרכים להעביר פרמטרים, ושווה להכיר את כולן:
// דוגמאות ניווט Shell
// 1. ניווט פשוט לדף
await Shell.Current.GoToAsync(nameof(AddTaskPage));
// 2. ניווט עם פרמטר ב-Query String
await Shell.Current.GoToAsync($"{nameof(TaskDetailPage)}?taskId=42");
// 3. ניווט עם מספר פרמטרים
await Shell.Current.GoToAsync(
$"{nameof(TaskDetailPage)}?taskId=42&mode=edit");
// 4. ניווט עם אובייקט מורכב דרך Dictionary
var navigationParams = new Dictionary<string, object>
{
{ "task", selectedTask },
{ "isNewTask", false }
};
await Shell.Current.GoToAsync(nameof(TaskDetailPage), navigationParams);
// 5. חזרה לדף הקודם
await Shell.Current.GoToAsync("..");
// 6. חזרה לדף השורש
await Shell.Current.GoToAsync("//TaskList");
// 7. ניווט אבסולוטי (מאפס את מחסנית הניווט)
await Shell.Current.GoToAsync("//TaskList/TaskDetail");
קבלת פרמטרים עם IQueryAttributable
הממשק IQueryAttributable הוא הדרך המומלצת לקבל נתונים בזמן ניווט. הוא נקי, מסודר, ופשוט לשימוש:
// דוגמה מפורטת לקבלת פרמטרים מניווט
public partial class TaskDetailViewModel : ObservableObject, IQueryAttributable
{
// נקרא אוטומטית כשהדף מקבל פרמטרי ניווט
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
// קבלת פרמטר string מ-Query String
if (query.TryGetValue("taskId", out var taskIdObj))
{
if (int.TryParse(taskIdObj?.ToString(), out int taskId))
{
_ = LoadTaskAsync(taskId);
}
}
// קבלת אובייקט מורכב מ-Dictionary
if (query.TryGetValue("task", out var taskObj)
&& taskObj is TaskItem task)
{
Task = task;
}
// קבלת פרמטר בוליאני
if (query.TryGetValue("isNewTask", out var isNewObj)
&& isNewObj is bool isNew)
{
IsEditing = isNew;
}
}
}
בניית ממשק המשתמש
עכשיו מגיע החלק הכיפי — לחבר את הכל ביחד ולבנות את ה-UI. נתחיל מדף רשימת המשימות הראשי:
<!-- Views/TaskListPage.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"
xmlns:vm="clr-namespace:TaskMaster.ViewModels"
xmlns:models="clr-namespace:TaskMaster.Models"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="TaskMaster.Views.TaskListPage"
x:DataType="vm:TaskListViewModel"
Title="המשימות שלי"
FlowDirection="RightToLeft">
<Shell.SearchHandler>
<SearchHandler
Placeholder="חפש משימה..."
Query="{Binding SearchText}" />
</Shell.SearchHandler>
<Grid RowDefinitions="Auto,*,Auto">
<Frame
Grid.Row="0"
Margin="16,8"
Padding="16"
BackgroundColor="{StaticResource PrimaryColor}"
CornerRadius="12"
HasShadow="True">
<HorizontalStackLayout Spacing="10">
<Label
Text=""
FontFamily="FluentIcons"
FontSize="28"
TextColor="White"
VerticalOptions="Center" />
<VerticalStackLayout>
<Label
Text="המשימות שלי"
FontSize="20"
FontAttributes="Bold"
TextColor="White" />
<Label
Text="{Binding Tasks.Count, StringFormat='{0} משימות'}"
FontSize="14"
TextColor="White"
Opacity="0.8" />
</VerticalStackLayout>
</HorizontalStackLayout>
</Frame>
<CollectionView
Grid.Row="1"
ItemsSource="{Binding Tasks}"
SelectionMode="None"
Margin="8,0">
<CollectionView.EmptyView>
<VerticalStackLayout
HorizontalOptions="Center"
VerticalOptions="Center"
Spacing="10">
<Label
Text=""
FontFamily="FluentIcons"
FontSize="64"
HorizontalOptions="Center"
TextColor="Gray" />
<Label
Text="אין משימות עדיין"
FontSize="18"
HorizontalOptions="Center"
TextColor="Gray" />
<Label
Text="לחץ על + כדי להוסיף משימה חדשה"
FontSize="14"
HorizontalOptions="Center"
TextColor="LightGray" />
</VerticalStackLayout>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:TaskItem">
<SwipeView>
<SwipeView.LeftItems>
<SwipeItems>
<SwipeItem
Text="מחק"
BackgroundColor="Red"
Command="{Binding
Source={RelativeSource AncestorType={x:Type vm:TaskListViewModel}},
Path=DeleteTaskCommand}"
CommandParameter="{Binding .}" />
</SwipeItems>
</SwipeView.LeftItems>
<Frame
Margin="8,4"
Padding="12"
CornerRadius="10"
HasShadow="True"
BackgroundColor="{AppThemeBinding
Light=White,
Dark={StaticResource DarkSurface}}">
<Frame.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding
Source={RelativeSource AncestorType={x:Type vm:TaskListViewModel}},
Path=GoToTaskDetailCommand}"
CommandParameter="{Binding .}" />
</Frame.GestureRecognizers>
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="12">
<CheckBox
Grid.Column="0"
IsChecked="{Binding IsCompleted}"
Color="{StaticResource PrimaryColor}"
VerticalOptions="Center" />
<VerticalStackLayout
Grid.Column="1"
Spacing="4"
VerticalOptions="Center">
<Label
Text="{Binding Title}"
FontSize="16"
FontAttributes="Bold"
LineBreakMode="TailTruncation" />
<Label
Text="{Binding Description}"
FontSize="13"
TextColor="Gray"
MaxLines="1"
LineBreakMode="TailTruncation" />
<HorizontalStackLayout Spacing="8">
<Label
Text="{Binding DueDate, StringFormat='{0:dd/MM/yyyy}'}"
FontSize="12"
TextColor="Gray" />
<Label
Text="{Binding Category}"
FontSize="12"
TextColor="{StaticResource PrimaryColor}" />
</HorizontalStackLayout>
</VerticalStackLayout>
<BoxView
Grid.Column="2"
WidthRequest="4"
HeightRequest="40"
CornerRadius="2"
VerticalOptions="Center" />
</Grid>
</Frame>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<ActivityIndicator
Grid.Row="1"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"
Color="{StaticResource PrimaryColor}"
HorizontalOptions="Center"
VerticalOptions="Center" />
<Button
Grid.Row="2"
Text="+ משימה חדשה"
Command="{Binding GoToAddTaskCommand}"
Margin="16"
Padding="16,12"
BackgroundColor="{StaticResource PrimaryColor}"
TextColor="White"
FontSize="16"
FontAttributes="Bold"
CornerRadius="25"
Shadow="{Shadow Brush=Black, Offset='2,4', Radius=8, Opacity=0.3}" />
</Grid>
</ContentPage>
ה-Code-Behind מינימלי לחלוטין — בדיוק כמו שצריך בארכיטקטורת MVVM:
// Views/TaskListPage.xaml.cs
namespace TaskMaster.Views;
using TaskMaster.ViewModels;
public partial class TaskListPage : ContentPage
{
private readonly TaskListViewModel _viewModel;
public TaskListPage(TaskListViewModel viewModel)
{
InitializeComponent();
BindingContext = _viewModel = viewModel;
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.LoadTasksCommand.Execute(null);
}
}
ודף הוספת המשימה:
<!-- Views/AddTaskPage.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"
xmlns:vm="clr-namespace:TaskMaster.ViewModels"
x:Class="TaskMaster.Views.AddTaskPage"
x:DataType="vm:AddTaskViewModel"
Title="משימה חדשה"
FlowDirection="RightToLeft">
<ScrollView Padding="16">
<VerticalStackLayout Spacing="16">
<VerticalStackLayout Spacing="4">
<Label Text="כותרת המשימה" FontSize="14" TextColor="Gray" />
<Entry
Text="{Binding Title}"
Placeholder="הזן כותרת..."
FontSize="16"
ClearButtonVisibility="WhileEditing" />
</VerticalStackLayout>
<VerticalStackLayout Spacing="4">
<Label Text="תיאור" FontSize="14" TextColor="Gray" />
<Editor
Text="{Binding Description}"
Placeholder="הוסף תיאור..."
HeightRequest="100"
FontSize="15" />
</VerticalStackLayout>
<VerticalStackLayout Spacing="4">
<Label Text="תאריך יעד" FontSize="14" TextColor="Gray" />
<DatePicker
Date="{Binding DueDate}"
MinimumDate="{Binding MinDate}"
FontSize="15" />
</VerticalStackLayout>
<VerticalStackLayout Spacing="4">
<Label Text="עדיפות" FontSize="14" TextColor="Gray" />
<Picker
ItemsSource="{Binding Priorities}"
SelectedIndex="{Binding SelectedPriority}"
Title="בחר עדיפות" />
</VerticalStackLayout>
<VerticalStackLayout Spacing="4">
<Label Text="קטגוריה" FontSize="14" TextColor="Gray" />
<Picker
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
Title="בחר קטגוריה" />
</VerticalStackLayout>
<Grid ColumnDefinitions="*,*" ColumnSpacing="12" Margin="0,16,0,0">
<Button
Grid.Column="0"
Text="ביטול"
Command="{Binding CancelCommand}"
BackgroundColor="Transparent"
TextColor="{StaticResource PrimaryColor}"
BorderColor="{StaticResource PrimaryColor}"
BorderWidth="1"
CornerRadius="10" />
<Button
Grid.Column="1"
Text="שמור משימה"
Command="{Binding SaveTaskCommand}"
BackgroundColor="{StaticResource PrimaryColor}"
TextColor="White"
CornerRadius="10" />
</Grid>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
שכבת הנתונים — SQLite
לאחסון מקומי נשתמש ב-SQLite דרך sqlite-net-pcl. הספרייה הזו מספקת API אסינכרוני פשוט עם מיפוי אוטומטי בין אובייקטים לטבלאות. אם אתם שואלים למה דווקא היא — זו הבחירה הנפוצה ביותר לאחסון מקומי ב-MAUI, ולא סתם.
הגדרת שירות מסד הנתונים
// Services/IDatabaseService.cs
using SQLite;
namespace TaskMaster.Services;
public interface IDatabaseService
{
Task<SQLiteAsyncConnection> GetConnectionAsync();
}
// Services/DatabaseService.cs
using SQLite;
using TaskMaster.Models;
namespace TaskMaster.Services;
public class DatabaseService : IDatabaseService
{
private SQLiteAsyncConnection? _connection;
private readonly string _dbPath;
public DatabaseService()
{
_dbPath = Path.Combine(
FileSystem.AppDataDirectory,
"taskmaster.db3");
}
public async Task<SQLiteAsyncConnection> GetConnectionAsync()
{
if (_connection is not null)
return _connection;
_connection = new SQLiteAsyncConnection(_dbPath, SQLiteOpenFlags.ReadWrite
| SQLiteOpenFlags.Create
| SQLiteOpenFlags.SharedCache);
await _connection.CreateTableAsync<TaskItem>();
await _connection.CreateTableAsync<TaskCategory>();
var count = await _connection.Table<TaskCategory>().CountAsync();
if (count == 0)
{
await SeedDefaultCategoriesAsync(_connection);
}
return _connection;
}
private async Task SeedDefaultCategoriesAsync(SQLiteAsyncConnection db)
{
var defaultCategories = new List<TaskCategory>
{
new() { Name = "כללי", IconGlyph = "", ColorHex = "#512BD4" },
new() { Name = "עבודה", IconGlyph = "", ColorHex = "#2196F3" },
new() { Name = "אישי", IconGlyph = "", ColorHex = "#4CAF50" },
new() { Name = "קניות", IconGlyph = "", ColorHex = "#FF9800" },
new() { Name = "בריאות", IconGlyph = "", ColorHex = "#F44336" },
new() { Name = "לימודים", IconGlyph = "", ColorHex = "#9C27B0" }
};
await db.InsertAllAsync(defaultCategories);
}
}
חלופה: Entity Framework Core עם SQLite
אם אתם מעדיפים ORM מלא יותר (ובפרויקטים גדולים יותר זה בהחלט הגיוני), אפשר להשתמש ב-Entity Framework Core:
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using TaskMaster.Models;
namespace TaskMaster.Data;
public class AppDbContext : DbContext
{
public DbSet<TaskItem> Tasks => Set<TaskItem>();
public DbSet<TaskCategory> Categories => Set<TaskCategory>();
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<TaskItem>()
.HasIndex(t => t.IsCompleted);
modelBuilder.Entity<TaskItem>()
.HasIndex(t => t.Category);
modelBuilder.Entity<TaskItem>()
.HasIndex(t => t.DueDate);
modelBuilder.Entity<TaskCategory>().HasData(
new TaskCategory { Id = 1, Name = "כללי", ColorHex = "#512BD4" },
new TaskCategory { Id = 2, Name = "עבודה", ColorHex = "#2196F3" },
new TaskCategory { Id = 3, Name = "אישי", ColorHex = "#4CAF50" }
);
}
}
// רישום ב-MauiProgram.cs:
// builder.Services.AddDbContext<AppDbContext>(options =>
// options.UseSqlite($"Data Source={Path.Combine(
// FileSystem.AppDataDirectory, "taskmaster.db")}"));
אז מה עדיף? sqlite-net קטנה יותר ומהירה יותר על מכשירים ניידים. EF Core נותן Migrations, שאילתות LINQ מורכבות ותמיכה ב-Relationships. לפרויקט קטן-בינוני כמו שלנו — sqlite-net מספיקה בהחלט. לפרויקט גדול עם מודל נתונים מורכב — שקלו EF Core.
רכיבים נוספים שעושים את ההבדל
ממירים (Converters)
ממירים מאפשרים להמיר נתונים בין המודל לתצוגה. דוגמה קלאסית — ממיר שמחזיר צבע לפי עדיפות המשימה:
// Converters/PriorityToColorConverter.cs
using System.Globalization;
namespace TaskMaster.Converters;
public class PriorityToColorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType,
object? parameter, CultureInfo culture)
{
if (value is int priority)
{
return priority switch
{
0 => Colors.Green, // נמוכה
1 => Colors.Orange, // בינונית
2 => Colors.Red, // גבוהה
_ => Colors.Gray
};
}
return Colors.Gray;
}
public object? ConvertBack(object? value, Type targetType,
object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class BoolToStrikethroughConverter : IValueConverter
{
public object? Convert(object? value, Type targetType,
object? parameter, CultureInfo culture)
{
if (value is bool isCompleted && isCompleted)
{
return TextDecorations.Strikethrough;
}
return TextDecorations.None;
}
public object? ConvertBack(object? value, Type targetType,
object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
רישום הממירים ב-App.xaml:
<!-- App.xaml -->
<Application
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="clr-namespace:TaskMaster.Converters"
x:Class="TaskMaster.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
<converters:PriorityToColorConverter x:Key="PriorityToColor" />
<converters:BoolToStrikethroughConverter x:Key="BoolToStrikethrough" />
<Color x:Key="PrimaryColor">#512BD4</Color>
<Color x:Key="PrimaryDarkColor">#3B1F99</Color>
<Color x:Key="DarkSurface">#1E1E2E</Color>
</ResourceDictionary>
</Application.Resources>
</Application>
מנגנון Messaging בין ViewModels
CommunityToolkit.Mvvm כוללת WeakReferenceMessenger — מנגנון שמאפשר ל-ViewModels "לדבר" ביניהם בלי להכיר אחד את השני ישירות. זה מאוד שימושי כשמשימה נוספת בדף אחד ואנחנו רוצים שהרשימה בדף אחר תתעדכן:
// הגדרת הודעה מותאמת
public class TaskChangedMessage : ValueChangedMessage<TaskItem>
{
public TaskChangeType ChangeType { get; }
public TaskChangedMessage(TaskItem task, TaskChangeType changeType)
: base(task)
{
ChangeType = changeType;
}
}
public enum TaskChangeType
{
Added,
Updated,
Deleted
}
// שליחת הודעה כשמשימה נוספת (ב-AddTaskViewModel)
await _taskService.AddTaskAsync(newTask);
WeakReferenceMessenger.Default.Send(
new TaskChangedMessage(newTask, TaskChangeType.Added));
// האזנה להודעות ב-TaskListViewModel
public TaskListViewModel(ITaskService taskService)
{
_taskService = taskService;
WeakReferenceMessenger.Default
.Register<TaskChangedMessage>(this, (recipient, message) =>
{
var vm = (TaskListViewModel)recipient;
MainThread.BeginInvokeOnMainThread(async () =>
{
switch (message.ChangeType)
{
case TaskChangeType.Added:
vm.Tasks.Add(message.Value);
break;
case TaskChangeType.Deleted:
var toRemove = vm.Tasks.FirstOrDefault(
t => t.Id == message.Value.Id);
if (toRemove is not null)
vm.Tasks.Remove(toRemove);
break;
case TaskChangeType.Updated:
await vm.LoadTasksAsync();
break;
}
vm.IsEmpty = vm.Tasks.Count == 0;
});
});
}
שיטות עבודה מומלצות
ביצועים
כמה דברים שלמדתי בדרך הקשה:
- CollectionView ולא ListView — CollectionView מספק ביצועי גלילה טובים בהרבה בזכות virtualization מובנה. אם אתם עדיין משתמשים ב-ListView, עברו.
- מינימום Bindings — כל Binding מוסיף תקורה. אם ערך לא משתנה, השתמשו ב-
x:StaticאוOneTimeBinding Mode. - Compiled Bindings תמיד — הוסיפו
x:DataTypeלכל דף ו-DataTemplate. בינדינג מקומפל מהיר פי 8-20 מבינדינג רגיל. כן, קראתם נכון. - אופטימיזציה של תמונות — גדלים מותאמים לפלטפורמה ופורמטים מודרניים כמו WebP.
- Lazy Loading — טענו נתונים ב-
OnAppearingולא בבנאי. ההבדל מורגש.
// דוגמה ל-Compiled Bindings — שיפור ביצועים משמעותי
<ContentPage
x:DataType="vm:TaskListViewModel">
<CollectionView ItemsSource="{Binding Tasks}">
<CollectionView.ItemTemplate>
<!-- חשוב: x:DataType גם ב-DataTemplate -->
<DataTemplate x:DataType="models:TaskItem">
<Label Text="{Binding Title}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
// טעינה עצלה
protected override async void OnAppearing()
{
base.OnAppearing();
if (_viewModel.Tasks.Count == 0)
{
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
}
}
בדיקות (Testing)
היופי של MVVM עם הזרקת תלויות הוא שבדיקות יחידה הופכות לפשוטות. הנה דוגמה עם xUnit ו-Moq:
// Tests/ViewModels/TaskListViewModelTests.cs
using Moq;
using TaskMaster.Models;
using TaskMaster.Services;
using TaskMaster.ViewModels;
namespace TaskMaster.Tests.ViewModels;
public class TaskListViewModelTests
{
private readonly Mock<ITaskService> _mockTaskService;
private readonly TaskListViewModel _viewModel;
public TaskListViewModelTests()
{
_mockTaskService = new Mock<ITaskService>();
_viewModel = new TaskListViewModel(_mockTaskService.Object);
}
[Fact]
public async Task LoadTasks_ShouldPopulateTasksList()
{
// Arrange
var testTasks = new List<TaskItem>
{
new() { Id = 1, Title = "משימה ראשונה", Priority = 2 },
new() { Id = 2, Title = "משימה שנייה", Priority = 1 },
new() { Id = 3, Title = "משימה שלישית", Priority = 0 }
};
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.ReturnsAsync(testTasks);
// Act
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
// Assert
Assert.Equal(3, _viewModel.Tasks.Count);
Assert.False(_viewModel.IsEmpty);
Assert.False(_viewModel.IsLoading);
}
[Fact]
public async Task LoadTasks_WhenEmpty_ShouldSetIsEmptyTrue()
{
_mockTaskService
.Setup(s => s.GetAllTasksAsync())
.ReturnsAsync(new List<TaskItem>());
await _viewModel.LoadTasksCommand.ExecuteAsync(null);
Assert.Empty(_viewModel.Tasks);
Assert.True(_viewModel.IsEmpty);
}
[Fact]
public async Task DeleteTask_ShouldRemoveFromList()
{
var task = new TaskItem { Id = 1, Title = "למחיקה" };
_viewModel.Tasks.Add(task);
_mockTaskService
.Setup(s => s.DeleteTaskAsync(1))
.ReturnsAsync(1);
_mockTaskService.Verify(
s => s.DeleteTaskAsync(1),
Times.Never);
Assert.Single(_viewModel.Tasks);
}
}
טיפול מרכזי בשגיאות
במקום לכתוב את אותו קוד טיפול בשגיאות בכל ViewModel, כדאי ליצור שירות התראות מרכזי. זה גם מאפשר מוקינג קל בבדיקות:
// Services/IAlertService.cs
namespace TaskMaster.Services;
public interface IAlertService
{
Task ShowAlertAsync(string title, string message, string cancel = "אישור");
Task<bool> ShowConfirmationAsync(string title, string message,
string accept = "אישור", string cancel = "ביטול");
Task ShowToastAsync(string message);
}
// Services/AlertService.cs
using CommunityToolkit.Maui.Alerts;
using CommunityToolkit.Maui.Core;
namespace TaskMaster.Services;
public class AlertService : IAlertService
{
public async Task ShowAlertAsync(string title, string message,
string cancel = "אישור")
{
if (Shell.Current is not null)
{
await Shell.Current.DisplayAlert(title, message, cancel);
}
}
public async Task<bool> ShowConfirmationAsync(string title, string message,
string accept = "אישור", string cancel = "ביטול")
{
if (Shell.Current is not null)
{
return await Shell.Current.DisplayAlert(
title, message, accept, cancel);
}
return false;
}
public async Task ShowToastAsync(string message)
{
var toast = Toast.Make(message, ToastDuration.Short, 14);
await toast.Show();
}
}
// שימוש ב-ViewModel
public partial class TaskListViewModel : ObservableObject
{
private readonly ITaskService _taskService;
private readonly IAlertService _alertService;
public TaskListViewModel(ITaskService taskService,
IAlertService alertService)
{
_taskService = taskService;
_alertService = alertService;
}
[RelayCommand]
private async Task LoadTasksAsync()
{
if (IsLoading) return;
try
{
IsLoading = true;
var tasks = await _taskService.GetAllTasksAsync();
Tasks = new ObservableCollection<TaskItem>(tasks);
IsEmpty = Tasks.Count == 0;
}
catch (SQLite.SQLiteException ex)
{
await _alertService.ShowAlertAsync(
"שגיאת מסד נתונים",
"לא ניתן לגשת למסד הנתונים. נסה להפעיל מחדש את האפליקציה.");
System.Diagnostics.Debug.WriteLine($"DB Error: {ex.Message}");
}
catch (Exception ex)
{
await _alertService.ShowAlertAsync(
"שגיאה",
"אירעה שגיאה בלתי צפויה. נסה שוב מאוחר יותר.");
System.Diagnostics.Debug.WriteLine($"Error: {ex.Message}");
}
finally
{
IsLoading = false;
}
}
}
טיפים נוספים
- Compiled Bindings בכל מקום — הוסיפו
x:DataTypeלכל ContentPage ו-DataTemplate. זה נותן גם ביצועים טובים וגם גילוי שגיאות בקומפילציה. - הפרדת שכבות — ה-ViewModel לעולם לא מכיר את ה-View ישירות. כל תקשורת עוברת דרך Bindings, Commands ו-Messaging.
- הימנעו מ-async void — תמיד
async Taskבמקוםasync void, למעט Event Handlers.[RelayCommand]מטפל בזה אוטומטית. - CancellationToken לפעולות ארוכות:
// CancellationToken עם AsyncRelayCommand
[RelayCommand(IncludeCancelCommand = true)]
private async Task LoadTasksAsync(CancellationToken cancellationToken)
{
IsLoading = true;
try
{
var tasks = await _taskService.GetAllTasksAsync();
cancellationToken.ThrowIfCancellationRequested();
Tasks = new ObservableCollection<TaskItem>(tasks);
}
catch (OperationCanceledException)
{
System.Diagnostics.Debug.WriteLine("טעינת המשימות בוטלה");
}
finally
{
IsLoading = false;
}
}
// ב-XAML:
// <Button Text="בטל טעינה" Command="{Binding LoadTasksCancelCommand}" />
- התאמה לגדלי מסך שונים — השתמשו ב-
AdaptiveTrigger:
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Phone">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Property="Grid.ColumnDefinitions"
Value="*" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Tablet">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="768" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Property="Grid.ColumnDefinitions"
Value="300,*" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
סיכום
בנינו אפליקציית .NET MAUI שלמה לניהול משימות עם שלושה דפוסים מרכזיים שכל מפתח MAUI צריך להכיר:
- MVVM עם CommunityToolkit.Mvvm —
ObservableObject,[ObservableProperty]ו-[RelayCommand]ליצירת ViewModels נקיים עם מינימום boilerplate. ה-Source Generators עושים את העבודה הקשה בשבילכם. - הזרקת תלויות — הגדרת שירותים ב-
MauiProgram.csעם Singleton ו-Transient לפי הצורך. מאפשר החלפת מימושים וכתיבת בדיקות. - ניווט Shell — נתיבים,
GoToAsync, העברת פרמטרים ו-IQueryAttributable. מערכת ניווט חזקה שפשוט עובדת.
כיסינו גם SQLite, ממירים, Messaging בין ViewModels, טיפול בשגיאות ובדיקות יחידה.
מה הלאה?
כמה כיוונים שכדאי לחקור:
- סנכרון ענן — שלבו Azure Mobile Apps או Firebase לסנכרון בין מכשירים.
- התראות — Plugin.LocalNotification או Firebase Cloud Messaging לתזכורות.
- Dark Mode —
AppThemeBindingלהתאמת צבעים אוטומטית. - אנימציות — CommunityToolkit.Maui לאנימציות שמשפרות את החוויה.
- CI/CD — GitHub Actions או Azure DevOps לבנייה ופריסה אוטומטית.
- .NET MAUI Blazor Hybrid — אם יש לכם רקע ב-Web, שילוב Blazor עם MAUI מאפשר שימוש חוזר ברכיבי Web באפליקציה נייטיבית.
.NET MAUI 10 הביא איתו ביצועים טובים יותר, APIs חדשים ותמיכה רחבה יותר. בשילוב עם הדפוסים שלמדנו — יש לכם את כל מה שצריך לבנות אפליקציות מקצועיות ברמה גבוהה. קחו את הקוד, התאימו אותו לפרויקט שלכם, ותתחילו לבנות.