إذا كنت مطور ويب تتقن HTML وCSS وC# وعندك رغبة في دخول عالم تطبيقات الموبايل والديسكتوب — فعندي لك خبر سار: مش لازم تتعلم XAML ولا تبدأ كل شيء من الصفر. تقنية Blazor Hybrid مع .NET MAUI تخليك تبني تطبيقات أصلية متعددة المنصات باستخدام مهاراتك الحالية في تطوير الويب. نفس مكونات Razor، نفس أنماط CSS، نفس منطق C# — لكن داخل تطبيق أصلي يشتغل على Android وiOS وWindows وmacOS.
والجزء الأجمل؟ تقدر تشارك نفس واجهة المستخدم بين تطبيق الويب وتطبيق الموبايل عبر مكتبة Razor Class Library مشتركة. يعني حرفياً تكتب الكود مرة وحدة وتنشره في كل مكان.
في هذا الدليل، بنمشي مع بعض خطوة بخطوة ونبني تطبيق Blazor Hybrid كامل باستخدام .NET MAUI 10. بنغطي كل شيء من الإعداد الأولي لحد الوصول لميزات الجهاز الأصلية ومشاركة الكود مع الويب. فخلّنا نبدأ!
ما هو Blazor Hybrid ولماذا يهمّك؟
Blazor Hybrid هو نمط تطوير يجمع بين قوة Blazor (إطار عمل الويب من Microsoft) وقدرات .NET MAUI الأصلية. الفكرة ببساطة: بدلاً من تشغيل مكونات Blazor في المتصفح عبر WebAssembly أو على خادم عبر SignalR، يتم تشغيلها مباشرةً داخل تطبيق أصلي عبر مكوّن BlazorWebView.
النقطة المهمة هنا — والكثير يخلطون فيها — إن الكود ما يشتغل في المتصفح ولا عبر خادم بعيد. كل شيء، من منطق الأعمال والوصول للبيانات ومعالجة الأحداث، يعمل كـ .NET أصلي على الجهاز مباشرةً. الجزء الوحيد اللي يستخدم تقنيات الويب هو واجهة المستخدم (HTML/CSS)، وهذي تُعرض داخل WebView مُدمج.
صراحةً، هذا الشي كان مفاجأة لي أول ما جربته — الأداء أفضل بكثير مما توقعت.
متى تختار Blazor Hybrid؟
- فريقك يتقن تطوير الويب: إذا عندك فريق خبرته في Blazor أو ASP.NET، فـ Blazor Hybrid يخليهم يبنون تطبيقات موبايل بدون منحنى تعلم صعب
- عندك تطبيق ويب Blazor موجود: تقدر تعيد استخدام مكونات Razor الموجودة مباشرةً في تطبيق الموبايل
- تطبيقات الأعمال الداخلية: لوحات تحكم، أنظمة إدارة مخزون، تطبيقات إدخال بيانات — يعني الأماكن اللي الوظيفية فيها أهم من المظهر الأصلي بنسبة 100%
- تريد تستهدف الويب والموبايل معاً: قاعدة كود واحدة تخدم كل المنصات
متى لا يكون الخيار الأمثل؟
ما هو حل سحري لكل شيء طبعاً. تجنّبه في هذي الحالات:
- تطبيقات تحتاج مظهر أصلي بالكامل لكل منصة (Material Design على Android، Cupertino على iOS)
- ألعاب أو تطبيقات تعتمد بشكل كبير على الرسوميات
- تطبيقات تحتاج أقصى أداء ممكن في واجهة المستخدم (مثل تطبيقات التداول اللحظي)
إعداد بيئة التطوير
قبل ما نبدأ، تأكد إن عندك هذي المتطلبات:
- Visual Studio 2022 الإصدار 17.12 أو أحدث مع حزمة عمل .NET MAUI
- .NET 10 SDK (أو .NET 9 كحد أدنى)
- Android SDK — يُثبّت تلقائياً مع Visual Studio فلا تقلق
- لتطوير iOS: تحتاج جهاز Mac متصل أو Mac مباشر (للأسف ما في طريقة ثانية حالياً)
إنشاء المشروع
أسهل طريقة هي استخدام قالب .NET MAUI Blazor Hybrid and Web App اللي يُنشئ لك ثلاثة مشاريع مترابطة دفعة واحدة:
dotnet new maui-blazor-web -n MyHybridApp
هذا الأمر ينشئ لك الهيكل التالي:
MyHybridApp/
├── MyHybridApp.Shared/ # مكتبة Razor المشتركة (RCL)
│ ├── Components/
│ │ ├── Pages/
│ │ │ ├── Home.razor
│ │ │ └── Counter.razor
│ │ └── Layout/
│ │ ├── MainLayout.razor
│ │ └── NavMenu.razor
│ └── wwwroot/
│ └── css/
├── MyHybridApp.Web/ # تطبيق Blazor للويب
│ └── Program.cs
├── MyHybridApp.Maui/ # تطبيق MAUI الأصلي
│ ├── MauiProgram.cs
│ ├── MainPage.xaml
│ └── wwwroot/
│ └── index.html
└── MyHybridApp.sln
لاحظ كيف القالب يفصل الكود المشترك في مشروع خاص. هذا التصميم هو اللي يخليك تشارك المكونات بين الويب والموبايل بسهولة.
فهم البنية الأساسية
ملف MauiProgram.cs — نقطة الانطلاق
هذا الملف هو قلب مشروع MAUI. فيه تُسجّل خدمات Blazor وتُضيف حقن التبعيات:
namespace MyHybridApp.Maui;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// تسجيل خدمات Blazor WebView
builder.Services.AddMauiBlazorWebView();
#if DEBUG
// أدوات المطور للتصحيح في وضع Debug
builder.Services.AddBlazorWebViewDeveloperTools();
#endif
// تسجيل خدماتك المخصصة
builder.Services.AddSingleton<IProductService, ProductService>();
builder.Services.AddTransient<IOrderService, OrderService>();
return builder.Build();
}
}
الكود واضح ومباشر. AddMauiBlazorWebView() هي السطر السحري اللي يربط Blazor بـ MAUI. وأدوات المطور في وضع Debug مفيدة جداً للتصحيح — أنصحك تخليها مفعّلة دائماً أثناء التطوير.
ملف MainPage.xaml — استضافة BlazorWebView
هذا الملف يحتوي على مكوّن BlazorWebView اللي يستضيف تطبيق Blazor بالكامل:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyHybridApp.Maui"
x:Class="MyHybridApp.Maui.MainPage">
<BlazorWebView x:Name="blazorWebView"
HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app"
ComponentType="{x:Type local:Components.Routes}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
خاصية HostPage تشير لملف HTML الجذري، وRootComponent يحدد مكوّن Blazor الجذري اللي بيُعرض داخل عنصر #app. هذا تقريباً الملف الوحيد من XAML اللي بتحتاج تتعامل معه — وهذي نقطة كبيرة لصالح Blazor Hybrid.
بناء مكوّنات مشتركة بين الويب والموبايل
هنا تبدأ المتعة الحقيقية.
القوة الأساسية لـ Blazor Hybrid تكمن في مكتبة Razor Class Library المشتركة. كل مكوّن تكتبه في هذي المكتبة يشتغل تلقائياً في تطبيق الويب وتطبيق الموبايل بدون أي تعديل.
مثال عملي: مكوّن قائمة المنتجات
خلّنا نبني مكوّن عرض منتجات يشتغل على كل المنصات:
@* ملف: MyHybridApp.Shared/Components/Pages/Products.razor *@
@page "/products"
@inject IProductService ProductService
<h1>المنتجات</h1>
@if (_products == null)
{
<p>جاري التحميل...</p>
}
else if (!_products.Any())
{
<div class="empty-state">
<p>لا توجد منتجات حالياً</p>
</div>
}
else
{
<div class="product-grid">
@foreach (var product in _products)
{
<div class="product-card">
<img src="@product.ImageUrl" alt="@product.Name" />
<h3>@product.Name</h3>
<p class="price">@product.Price.ToString("C")</p>
<button class="btn-primary"
@onclick="() => AddToCart(product)">
أضف للسلة
</button>
</div>
}
</div>
}
@code {
private List<Product>? _products;
protected override async Task OnInitializedAsync()
{
_products = await ProductService.GetProductsAsync();
}
private async Task AddToCart(Product product)
{
await ProductService.AddToCartAsync(product.Id);
}
}
لاحظ إن المكوّن ما فيه أي شيء خاص بمنصة معينة — نفس الكود بيشتغل على الويب وعلى الموبايل بدون تغيير.
واجهة الخدمة المشتركة
المفتاح لمشاركة الكود هو استخدام الواجهات (Interfaces). تعرّف الواجهة في المكتبة المشتركة، ثم توفر تطبيق مختلف لكل منصة. هذا نمط كلاسيكي في عالم .NET وهو اللي يخلي السحر يحصل:
// ملف: MyHybridApp.Shared/Services/IProductService.cs
public interface IProductService
{
Task<List<Product>> GetProductsAsync();
Task AddToCartAsync(int productId);
Task<int> GetCartCountAsync();
}
في تطبيق الويب، الخدمة تستدعي API عبر HTTP:
// ملف: MyHybridApp.Web/Services/WebProductService.cs
public class WebProductService : IProductService
{
private readonly HttpClient _http;
public WebProductService(HttpClient http) => _http = http;
public async Task<List<Product>> GetProductsAsync()
=> await _http.GetFromJsonAsync<List<Product>>("api/products")
?? new();
public async Task AddToCartAsync(int productId)
=> await _http.PostAsJsonAsync("api/cart", new { productId });
public async Task<int> GetCartCountAsync()
=> await _http.GetFromJsonAsync<int>("api/cart/count");
}
بينما في تطبيق MAUI، الخدمة توصل للبيانات محلياً عبر SQLite — وهذا يعني سرعة فائقة وعمل بدون إنترنت:
// ملف: MyHybridApp.Maui/Services/MauiProductService.cs
public class MauiProductService : IProductService
{
private readonly LocalDatabase _db;
public MauiProductService(LocalDatabase db) => _db = db;
public async Task<List<Product>> GetProductsAsync()
=> await _db.GetAllAsync<Product>();
public async Task AddToCartAsync(int productId)
{
var cartItem = new CartItem { ProductId = productId, Quantity = 1 };
await _db.InsertAsync(cartItem);
}
public async Task<int> GetCartCountAsync()
=> await _db.CountAsync<CartItem>();
}
شفت الجمال؟ نفس الواجهة، تطبيقين مختلفين، ومكوّن Razor واحد ما يعرف (ولا يحتاج يعرف) أي منصة يشتغل عليها.
الوصول إلى ميزات الجهاز الأصلية
من أقوى مزايا Blazor Hybrid — وهذي اللي تفرقه عن Blazor WebAssembly بشكل كبير — إنك تقدر توصل لكل ميزات الجهاز الأصلية مباشرةً من كود C#. الكاميرا، GPS، مستشعرات الحركة، التخزين الآمن، الإشعارات... كل شيء متاح بدون جسر JavaScript أو مكتبات خارجية.
مثال: استخدام الموقع الجغرافي
أولاً، نعرّف واجهة للخدمة في المكتبة المشتركة:
// ملف: MyHybridApp.Shared/Services/IDeviceLocationService.cs
public interface IDeviceLocationService
{
Task<LocationResult?> GetCurrentLocationAsync();
bool IsSupported { get; }
}
public record LocationResult(double Latitude, double Longitude, string? Address);
ثم ننفّذها في مشروع MAUI باستخدام واجهات .NET MAUI الأصلية:
// ملف: MyHybridApp.Maui/Services/MauiLocationService.cs
public class MauiLocationService : IDeviceLocationService
{
public bool IsSupported => true;
public async Task<LocationResult?> GetCurrentLocationAsync()
{
try
{
var status = await Permissions.CheckStatusAsync
<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync
<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
return null;
}
var location = await Geolocation.Default.GetLocationAsync(
new GeolocationRequest(GeolocationAccuracy.Medium,
TimeSpan.FromSeconds(10)));
if (location == null) return null;
var placemarks = await Geocoding.Default.GetPlacemarksAsync(
location.Latitude, location.Longitude);
var address = placemarks?.FirstOrDefault();
var addressText = address != null
? $"{address.Thoroughfare}, {address.Locality}"
: null;
return new LocationResult(
location.Latitude,
location.Longitude,
addressText);
}
catch (Exception)
{
return null;
}
}
}
لاحظ كيف نتحقق من الصلاحيات أولاً ثم نطلبها إذا ما كانت ممنوحة — هذا مهم جداً وإلا التطبيق بيرمي استثناء على كثير من الأجهزة.
وفي تطبيق الويب، نوفر تطبيق بديل بسيط:
// ملف: MyHybridApp.Web/Services/WebLocationService.cs
public class WebLocationService : IDeviceLocationService
{
public bool IsSupported => false;
public Task<LocationResult?> GetCurrentLocationAsync()
=> Task.FromResult<LocationResult?>(null);
}
أخيراً، نستخدم الخدمة في مكوّن Razor المشترك — والمكوّن ذكي بما فيه الكفاية إنه يعرض زر الموقع بس لما يكون مدعوم:
@inject IDeviceLocationService LocationService
@if (LocationService.IsSupported)
{
<button class="btn-location" @onclick="GetLocation">
📍 تحديد موقعي
</button>
@if (_location != null)
{
<div class="location-info">
<p>خط العرض: @_location.Latitude</p>
<p>خط الطول: @_location.Longitude</p>
@if (_location.Address != null)
{
<p>العنوان: @_location.Address</p>
}
</div>
}
}
@code {
private LocationResult? _location;
private async Task GetLocation()
{
_location = await LocationService.GetCurrentLocationAsync();
}
}
التعامل مع أوضاع العرض (Render Modes)
هذي من النقاط اللي تسبب إرباك للمطورين الجدد على Blazor Hybrid.
المشكلة ببساطة: تطبيق MAUI ما يدعم أوضاع العرض (Render Modes) لأن كل شيء يعمل أصلاً بشكل تفاعلي. لكن تطبيق الويب يحتاج تحدد وضع العرض. فكيف نشارك نفس المكونات بينهم؟
الحل الرسمي من Microsoft هو استخدام فئة مساعدة في المكتبة المشتركة:
// ملف: MyHybridApp.Shared/InteractiveRenderSettings.cs
public static class InteractiveRenderSettings
{
public static IComponentRenderMode? InteractiveServer { get; set; } =
RenderMode.InteractiveServer;
public static IComponentRenderMode? InteractiveAuto { get; set; } =
RenderMode.InteractiveAuto;
public static IComponentRenderMode? InteractiveWasm { get; set; } =
RenderMode.InteractiveWebAssembly;
// يُستدعى من MauiProgram.cs لتعطيل أوضاع العرض
public static void ConfigureBlazorHybridRenderModes()
{
InteractiveServer = null;
InteractiveAuto = null;
InteractiveWasm = null;
}
}
وفي ملف MauiProgram.cs، كل اللي عليك تسويه هو استدعاء التهيئة في البداية:
public static MauiApp CreateMauiApp()
{
// تعطيل أوضاع العرض التفاعلية في MAUI
InteractiveRenderSettings.ConfigureBlazorHybridRenderModes();
var builder = MauiApp.CreateBuilder();
// ... باقي الإعداد
}
حل بسيط وفعّال. بمجرد ما تضبطه مرة، ما تحتاج تفكر فيه مرة ثانية.
إدارة الحالة والتنقل
التنقل بين الصفحات
التنقل في Blazor Hybrid يعمل بنفس طريقة Blazor العادية تماماً باستخدام NavigationManager:
@inject NavigationManager Navigation
<button @onclick='() => Navigation.NavigateTo("/products/details/5")'>
عرض التفاصيل
</button>
لكن في نقطة مهمة لازم تنتبه لها: التنقل يحصل داخل WebView فقط. يعني لو تبي تفتح رابط خارجي في المتصفح الحقيقي، لازم تستخدم خدمة Launcher من MAUI:
// فتح رابط في المتصفح الخارجي
await Launcher.Default.OpenAsync("https://example.com");
هذي غلطة شائعة — كثير من المطورين يستخدمون NavigateTo لروابط خارجية ويتفاجؤون إن الصفحة تنفتح داخل WebView بدل المتصفح.
إدارة الحالة المشتركة
لإدارة الحالة بين المكونات، نمط خدمة الحالة مع حقن التبعيات يشتغل بشكل ممتاز:
// ملف: MyHybridApp.Shared/State/AppState.cs
public class AppState
{
public event Action? OnChange;
private int _cartItemCount;
public int CartItemCount
{
get => _cartItemCount;
set
{
_cartItemCount = value;
NotifyStateChanged();
}
}
private bool _isAuthenticated;
public bool IsAuthenticated
{
get => _isAuthenticated;
set
{
_isAuthenticated = value;
NotifyStateChanged();
}
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
// التسجيل في MauiProgram.cs و Program.cs
builder.Services.AddSingleton<AppState>();
سجّله كـ Singleton عشان كل المكونات تشارك نفس النسخة. مع نمط الأحداث (OnChange)، أي مكوّن يقدر يستمع للتغييرات ويحدّث واجهته تلقائياً.
تنسيق CSS متجاوب للويب والموبايل
من التحديات العملية — واللي صراحة أخذت مني وقت أكثر مما توقعت — هي توفير تجربة مستخدم كويسة على أحجام شاشات مختلفة. لأن نفس الكود يشتغل على موبايل بشاشة 6 بوصة وعلى ديسكتوب بشاشة 27 بوصة.
إليك بعض الممارسات اللي وجدتها مفيدة:
/* ملف: MyHybridApp.Shared/wwwroot/css/app.css */
/* تصميم قاعدي يعمل على كل المنصات */
.product-grid {
display: grid;
gap: 1rem;
padding: 1rem;
}
/* شاشات صغيرة (موبايل) */
@media (max-width: 768px) {
.product-grid {
grid-template-columns: 1fr;
}
.product-card {
padding: 0.75rem;
}
}
/* شاشات متوسطة (تابلت) */
@media (min-width: 769px) and (max-width: 1024px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* شاشات كبيرة (ديسكتوب) */
@media (min-width: 1025px) {
.product-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* تكييف لأوضاع الشاشة المختلفة */
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a2e;
--text-color: #e0e0e0;
--card-bg: #16213e;
}
}
@media (prefers-color-scheme: light) {
:root {
--bg-color: #ffffff;
--text-color: #333333;
--card-bg: #f8f9fa;
}
}
نصيحة إضافية: استخدم الخصائص المنطقية في CSS (مثل margin-inline-start بدل margin-left) عشان تدعم اللغات ذات الاتجاه من اليمين لليسار بشكل تلقائي.
النشر والتوزيع
بعد ما تخلص من بناء التطبيق وتتأكد إن كل شيء يشتغل، الخطوة الأخيرة هي النشر. والعملية مباشرة لكل منصة:
بناء تطبيق Android
dotnet publish -f net10.0-android -c Release
بناء تطبيق iOS
dotnet publish -f net10.0-ios -c Release
بناء تطبيق Windows
dotnet publish -f net10.0-windows10.0.19041.0 -c Release
للنشر على متاجر التطبيقات، بتحتاج توقّع التطبيق رقمياً. في Android مثلاً، أضف التالي في ملف .csproj:
<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and
'$(Configuration)' == 'Release'">
<AndroidKeyStore>true</AndroidKeyStore>
<AndroidSigningKeyStore>myapp.keystore</AndroidSigningKeyStore>
<AndroidSigningKeyAlias>myapp</AndroidSigningKeyAlias>
<AndroidSigningKeyPass>YOUR_KEY_PASS</AndroidSigningKeyPass>
<AndroidSigningStorePass>YOUR_STORE_PASS</AndroidSigningStorePass>
</PropertyGroup>
(طبعاً لا تحط كلمات المرور مباشرة في الملف في بيئة إنتاجية — استخدم متغيرات البيئة أو أداة إدارة أسرار.)
نصائح لتحسين الأداء
Blazor Hybrid يعمل بشكل ممتاز في معظم الحالات، لكن إذا حسّيت إن التطبيق بطيء شوي، جرّب هذي النصائح:
- استخدم التحميل الكسول: لا تحمّل كل المكونات مقدماً. استخدم التحميل الديناميكي للمكونات الثقيلة
- قلّل عمليات إعادة العرض: استخدم
ShouldRender()و@keyلتجنب إعادة عرض المكونات بدون داعٍ — هذي بالذات تفرق كثير مع القوائم الطويلة - فعّل التجميع المسبق AOT: يحسّن أداء بدء التشغيل بشكل ملحوظ خاصةً على iOS
- استخدم التحكمات الأصلية عند الحاجة: لعرض قوائم طويلة جداً،
CollectionViewالأصلي أسرع بكثير من قائمة HTML - قلّل استدعاءات JavaScript: تجنب المراوحة المتكررة بين .NET وJavaScript عبر JS Interop
<!-- تفعيل AOT في ملف .csproj -->
<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
تنبيه سريع: AOT يزيد حجم التطبيق بشكل ملحوظ (أحياناً ضعف الحجم أو أكثر)، فوازن بين الأداء والحجم حسب احتياجات تطبيقك.
الأسئلة الشائعة
هل يعمل تطبيق Blazor Hybrid بدون اتصال بالإنترنت؟
نعم، وهذي من أقوى نقاطه. على عكس Blazor Server اللي يحتاج اتصال دائم بالخادم، تطبيقات Blazor Hybrid تشتغل بالكامل على الجهاز. الكود والواجهة والبيانات المحلية كلها متوفرة بدون إنترنت. تحتاج الاتصال بس إذا تطبيقك يستدعي API خارجي.
ما الفرق بين Blazor Hybrid وBlazor WebAssembly؟
Blazor WebAssembly يشتغل داخل المتصفح ومحدود بقدراته. أما Blazor Hybrid فيشتغل كتطبيق أصلي بصلاحيات كاملة — يوصل لنظام الملفات والكاميرا والمستشعرات وكل واجهات الجهاز. الأداء أيضاً أفضل لأن الكود يعمل على .NET Runtime أصلي وليس عبر WebAssembly.
هل يمكنني مشاركة 100% من الكود بين الويب والموبايل؟
عملياً، تقدر تشارك معظم الكود (واجهة المستخدم والمنطق التجاري)، لكن بتحتاج تطبيقات مختلفة للخدمات الخاصة بكل منصة مثل الكاميرا أو التخزين المحلي. النمط الموصى به — واللي شرحناه في هذا الدليل — هو استخدام واجهات في المكتبة المشتركة وحقن التطبيقات المناسبة لكل منصة.
كيف أتعامل مع دعم RTL في واجهة Blazor Hybrid؟
بما إن الواجهة مبنية بـ HTML وCSS، فدعم RTL سهل جداً. أضف dir="rtl" على عنصر HTML الجذري واستخدم الخصائص المنطقية في CSS. هذا بصراحة أسهل بكثير من التعامل مع FlowDirection في XAML.
هل Blazor Hybrid مناسب للتطبيقات الإنتاجية في 2026؟
بكل تأكيد. مع .NET 10 (إصدار LTS بدعم 3 سنوات)، Blazor Hybrid صار ناضج بما يكفي للإنتاج. شركات كبيرة تستخدمه لتطبيقات أعمال داخلية ولوحات تحكم وأدوات إدارة. والقالب المشترك (Hybrid + Web) في .NET 9/10 يخلي البداية أسهل من أي وقت.