إذا كنت تبني تطبيقاً بـ .NET MAUI، فأنت حتماً ستصطدم بسؤال جوهري من أول أسبوع: كيف أنظّم التنقل بين الصفحات بشكل نظيف؟ الجواب المختصر: Shell.
لكن الجواب المختصر وحده لا يكفي، صح؟ لهذا سنغوص في هذا الدليل في كل التفاصيل — من إعداد المسارات الأساسية، مروراً بتمرير البيانات بين الصفحات بأمان مع NativeAOT، وصولاً إلى تطبيق الربط العميق (Deep Linking) على Android وiOS.
الدليل مبني على .NET MAUI 10 (إصدار LTS الأحدث)، ويأخذ بعين الاعتبار التغييرات الأخيرة مثل دعم Trim Safety وأفضل الممارسات مع MVVM. إذا كنت لا تزال على إصدار أقدم، معظم المفاهيم تنطبق، لكن بعض التفاصيل الدقيقة قد تختلف.
ما هو Shell في .NET MAUI؟
بصراحة، أبسط طريقة لفهم Shell هي أن تتخيله كـ "الهيكل العظمي" لتطبيقك. بدلاً من أن تبني كل شيء يدوياً — القائمة الجانبية (Flyout)، التبويبات السفلية (Tabs)، شريط التنقل (Navigation Bar) — يوفر لك Shell كل هذا في مكان واحد.
وهذا يوفر عليك وقتاً كبيراً.
أبرز ما يقدمه Shell:
- مكان واحد لوصف التسلسل الهرمي المرئي: بدلاً من إنشاء صفحات متعددة وربطها يدوياً، تُعرّف كل شيء في ملف واحد
- نظام تنقل مبني على URI: تنتقل بين الصفحات باستخدام مسارات نصية مشابهة لعناوين الويب
- القائمة الجانبية والتبويبات مدمجة: لا حاجة لبنائها من الصفر
- محرك بحث مدمج (SearchHandler): مع واجهات برمجية جديدة في .NET 10 للتحكم بلوحة المفاتيح
- التنقل للخلف بمرونة: يمكنك الرجوع أكثر من صفحة واحدة دون المرور بكل الصفحات في المكدس
إعداد بنية Shell الأساسية
كل تطبيق .NET MAUI يبدأ بملف AppShell.xaml. هذا الملف هو المكان الذي تُعرّف فيه صفحات التطبيق وطريقة تنظيمها.
لنبدأ بمثال عملي — تطبيق تجارة إلكترونية بسيط:
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyShopApp.Views"
x:Class="MyShopApp.AppShell"
FlyoutBehavior="Flyout">
<!-- التبويبات الرئيسية -->
<FlyoutItem Title="الرئيسية"
Icon="home.png"
Route="home">
<ShellContent ContentTemplate="{DataTemplate views:HomePage}" />
</FlyoutItem>
<FlyoutItem Title="المنتجات"
Icon="products.png"
Route="products">
<Tab Title="الكل" Route="all">
<ShellContent ContentTemplate="{DataTemplate views:AllProductsPage}" />
</Tab>
<Tab Title="المفضلة" Route="favorites">
<ShellContent ContentTemplate="{DataTemplate views:FavoritesPage}" />
</Tab>
</FlyoutItem>
<FlyoutItem Title="سلة التسوق"
Icon="cart.png"
Route="cart">
<ShellContent ContentTemplate="{DataTemplate views:CartPage}" />
</FlyoutItem>
<FlyoutItem Title="حسابي"
Icon="profile.png"
Route="profile">
<ShellContent ContentTemplate="{DataTemplate views:ProfilePage}" />
</FlyoutItem>
</Shell>
لاحظ كيف أن كل FlyoutItem يحتوي على خاصية Route. هذه المسارات هي مفتاح نظام التنقل — سنستخدمها لاحقاً للانتقال بين الصفحات برمجياً.
متى تستخدم FlyoutItem مقابل TabBar؟
القاعدة بسيطة وصريحة:
- FlyoutItem: عندما يحتاج تطبيقك قائمة جانبية (hamburger menu) — مناسب للتطبيقات ذات الأقسام الكثيرة
- TabBar: عندما تريد تبويبات سفلية فقط بدون قائمة جانبية — مناسب للتطبيقات الأبسط
هذا مثال سريع باستخدام TabBar:
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyShopApp.Views"
x:Class="MyShopApp.AppShell">
<TabBar>
<Tab Title="الرئيسية" Icon="home.png" Route="home">
<ShellContent ContentTemplate="{DataTemplate views:HomePage}" />
</Tab>
<Tab Title="المنتجات" Icon="products.png" Route="products">
<ShellContent ContentTemplate="{DataTemplate views:AllProductsPage}" />
</Tab>
<Tab Title="سلة التسوق" Icon="cart.png" Route="cart">
<ShellContent ContentTemplate="{DataTemplate views:CartPage}" />
</Tab>
</TabBar>
</Shell>
فهم نظام المسارات (Routes) بعمق
نظام المسارات في Shell مستوحى من عناوين الويب (URLs). كل صفحة لها مسار، وتنتقل إليها بتحديد هذا المسار. هناك نوعان رئيسيان عليك أن تفهمهما جيداً.
المسارات المرئية (Visual Hierarchy Routes)
هذه تُعرّف مباشرة في AppShell.xaml وتكون جزءاً من البنية المرئية. من المثال السابق، تحصل تلقائياً على هذه المسارات:
// المسارات المطلقة المتاحة:
//home
//products/all
//products/favorites
//cart
//profile
المسارات المسجّلة (Registered Routes)
الصفحات التي لا تظهر في القائمة الجانبية أو التبويبات — مثل صفحات التفاصيل أو الإعدادات — تحتاج تسجيلاً يدوياً في ملف AppShell.xaml.cs:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// تسجيل الصفحات التفصيلية
Routing.RegisterRoute("productdetails", typeof(ProductDetailPage));
Routing.RegisterRoute("checkout", typeof(CheckoutPage));
Routing.RegisterRoute("orderconfirmation", typeof(OrderConfirmationPage));
Routing.RegisterRoute("settings", typeof(SettingsPage));
Routing.RegisterRoute("editprofile", typeof(EditProfilePage));
}
}
تنبيه: لا تسجّل مسارات بأسماء مكررة. إذا حاولت تسجيل مسار موجود بالفعل، سيُطلق ArgumentException فوراً عند بدء التطبيق — وهي مشكلة مزعجة لأنها تمنع التطبيق من الفتح أصلاً.
التنقل البرمجي باستخدام GoToAsync
كل عمليات التنقل في Shell تتم عبر أسلوب واحد: Shell.Current.GoToAsync(). الأمر أبسط مما تتوقع. دعنا نستعرض السيناريوهات المختلفة.
التنقل المطلق
يبدأ بـ // ويُعيد تعيين مكدس التنقل بالكامل:
// الانتقال إلى الصفحة الرئيسية (يمسح كل المكدس)
await Shell.Current.GoToAsync("//home");
// الانتقال إلى المنتجات المفضلة
await Shell.Current.GoToAsync("//products/favorites");
التنقل النسبي
يُضيف صفحة فوق المكدس الحالي (push عادي):
// الانتقال إلى تفاصيل المنتج (يُضاف فوق الصفحة الحالية)
await Shell.Current.GoToAsync("productdetails");
// الانتقال إلى صفحة الدفع
await Shell.Current.GoToAsync("checkout");
الرجوع للخلف
استخدم .. للرجوع صفحة واحدة، أو ../.. للرجوع صفحتين. بسيطة:
// الرجوع صفحة واحدة
await Shell.Current.GoToAsync("..");
// الرجوع صفحتين
await Shell.Current.GoToAsync("../..");
// الرجوع مع تمرير بيانات
await Shell.Current.GoToAsync($"..?result=success");
التنقل مع التحريك (Animation)
يمكنك التحكم في حركة الانتقال بين الصفحات:
// بدون حركة انتقالية
await Shell.Current.GoToAsync("productdetails", animate: false);
// مع حركة انتقالية (الافتراضي)
await Shell.Current.GoToAsync("productdetails", animate: true);
تمرير البيانات بين الصفحات: الطريقة الصحيحة
هذا من أهم الأجزاء في المقال — وصراحةً، هو المكان الذي يقع فيه كثير من المطورين في أخطاء. خاصة مع .NET MAUI 10 الذي يدعم NativeAOT والتقليم الكامل (Full Trimming)، ليست كل الطرق آمنة بعد الآن.
دعنا نستعرضها بالترتيب من الأفضل إلى الأقل تفضيلاً.
الطريقة الأولى: IQueryAttributable (الأفضل والأكثر أماناً)
هذه الطريقة المُوصى بها رسمياً من Microsoft. تعمل مع NativeAOT والتقليم الكامل، وتتكامل بشكل ممتاز مع نمط MVVM. أنصح باستخدامها دائماً كخيار افتراضي.
// في الصفحة المُرسِلة — تمرير كائن كامل
var selectedProduct = new Product
{
Id = 42,
Name = "هاتف Galaxy S26",
Price = 3999.99m
};
var navigationParams = new Dictionary<string, object>
{
{ "product", selectedProduct }
};
await Shell.Current.GoToAsync("productdetails", navigationParams);
وفي ViewModel الخاص بالصفحة المُستقبِلة:
public class ProductDetailViewModel : ObservableObject, IQueryAttributable
{
[ObservableProperty]
private Product _product;
[ObservableProperty]
private bool _isLoading;
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("product", out var value) && value is Product product)
{
Product = product;
}
}
}
يمكنك أيضاً تطبيق IQueryAttributable على الصفحة نفسها (وليس فقط ViewModel). هذا مفيد إذا كنت تحتاج تعديل عنوان الصفحة مثلاً:
public partial class ProductDetailPage : ContentPage, IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("product", out var value) && value is Product product)
{
Title = product.Name;
}
}
}
الطريقة الثانية: ShellNavigationQueryParameters (للاستخدام لمرة واحدة)
هذه مشابهة للطريقة الأولى لكن مع فرق جوهري: البيانات تُحذف تلقائياً من الذاكرة بعد معالجتها. وهذا بالضبط ما تريده في أغلب الحالات.
var parameters = new ShellNavigationQueryParameters
{
{ "order", currentOrder },
{ "paymentMethod", "credit_card" }
};
await Shell.Current.GoToAsync("checkout", parameters);
الفرق بين Dictionary وShellNavigationQueryParameters:
- Dictionary: البيانات تبقى في الذاكرة طالما الصفحة موجودة في مكدس التنقل. عند الرجوع إلى الصفحة، يُستدعى
ApplyQueryAttributesمرة أخرى بنفس البيانات القديمة — وهذا قد يسبب سلوكاً غريباً - ShellNavigationQueryParameters: البيانات تُستخدم مرة واحدة فقط ثم تُمسح. نظيف ومرتب
الطريقة الثالثة: معاملات URI النصية (للقيم البسيطة فقط)
إذا كنت تمرر مجرد رقم معرّف أو قيمة نصية بسيطة، هذه الطريقة تفي بالغرض:
// تمرير معرّف المنتج كنص في URI
int productId = 42;
await Shell.Current.GoToAsync($"productdetails?id={productId}");
// في ViewModel المستقبل
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("id", out var value))
{
int id = int.Parse(value.ToString());
// جلب المنتج من قاعدة البيانات باستخدام المعرّف
LoadProduct(id);
}
}
الطريقة التي يجب تجنبها: QueryPropertyAttribute
تحذير مهم: لا تستخدم [QueryProperty] في المشاريع الجديدة. هذه الطريقة تعتمد على الانعكاس (Reflection) وهي غير آمنة مع التقليم الكامل وNativeAOT. Microsoft نفسها أوصت بالتخلي عنها:
// ❌ لا تستخدم هذا — غير آمن مع NativeAOT
[QueryProperty(nameof(ProductId), "id")]
public partial class ProductDetailPage : ContentPage
{
public string ProductId { get; set; }
}
// ✅ استخدم هذا بدلاً منه
public partial class ProductDetailPage : ContentPage, IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query) { ... }
}
بناء خدمة تنقل مركزية مع MVVM
إذا كنت تعمل على تطبيق حقيقي (وليس مجرد مثال تعليمي)، فلا تستدعِ Shell.Current.GoToAsync() مباشرة من ViewModels. الطريقة الأنظف هي بناء خدمة تنقل يمكن حقنها عبر Dependency Injection.
لماذا؟ لأن هذا يجعل اختبار الوحدات (Unit Testing) أسهل بكثير — يمكنك إنشاء mock للخدمة بدلاً من الاعتماد على Shell الفعلي في الاختبارات.
تعريف واجهة خدمة التنقل
public interface INavigationService
{
Task GoToAsync(string route);
Task GoToAsync(string route, IDictionary<string, object> parameters);
Task GoBackAsync();
Task GoBackAsync(IDictionary<string, object> parameters);
}
تنفيذ الخدمة
public class ShellNavigationService : INavigationService
{
public async Task GoToAsync(string route)
{
await Shell.Current.GoToAsync(route);
}
public async Task GoToAsync(string route, IDictionary<string, object> parameters)
{
await Shell.Current.GoToAsync(route, parameters);
}
public async Task GoBackAsync()
{
await Shell.Current.GoToAsync("..");
}
public async Task GoBackAsync(IDictionary<string, object> parameters)
{
await Shell.Current.GoToAsync("..", parameters);
}
}
تسجيل الخدمة في MauiProgram
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// تسجيل خدمة التنقل كـ Singleton
builder.Services.AddSingleton<INavigationService, ShellNavigationService>();
// تسجيل ViewModels
builder.Services.AddTransient<ProductListViewModel>();
builder.Services.AddTransient<ProductDetailViewModel>();
builder.Services.AddTransient<CartViewModel>();
// تسجيل الصفحات
builder.Services.AddTransient<ProductDetailPage>();
builder.Services.AddTransient<CartPage>();
return builder.Build();
}
}
استخدام الخدمة في ViewModel
public partial class ProductListViewModel : ObservableObject
{
private readonly INavigationService _navigationService;
public ProductListViewModel(INavigationService navigationService)
{
_navigationService = navigationService;
}
[RelayCommand]
private async Task ViewProductDetails(Product product)
{
var parameters = new ShellNavigationQueryParameters
{
{ "product", product }
};
await _navigationService.GoToAsync("productdetails", parameters);
}
[RelayCommand]
private async Task OpenCart()
{
await _navigationService.GoToAsync("//cart");
}
}
التعامل مع أحداث التنقل
Shell يوفر حدثين مهمين يمكنك الاستفادة منهما — وأحدهما مفيد جداً لسيناريوهات مثل التحقق من تسجيل الدخول قبل الوصول لصفحات معينة:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// تسجيل المسارات
Routing.RegisterRoute("productdetails", typeof(ProductDetailPage));
// الاستماع لأحداث التنقل
Navigating += OnNavigating;
Navigated += OnNavigated;
}
private void OnNavigating(object sender, ShellNavigatingEventArgs e)
{
// يُطلق قبل التنقل — يمكنك إلغاء التنقل هنا
if (e.Target.Location.OriginalString.Contains("checkout"))
{
// مثال: التحقق من تسجيل الدخول قبل الانتقال للدفع
if (!IsUserLoggedIn())
{
e.Cancel(); // إلغاء التنقل
Shell.Current.GoToAsync("//login");
}
}
}
private void OnNavigated(object sender, ShellNavigatedEventArgs e)
{
// يُطلق بعد اكتمال التنقل
System.Diagnostics.Debug.WriteLine(
$"انتقل من: {e.Previous?.Location} إلى: {e.Current?.Location}");
}
private bool IsUserLoggedIn()
{
// منطق التحقق من تسجيل الدخول
return Preferences.ContainsKey("auth_token");
}
}
تخصيص زر الرجوع وشريط التنقل
أحياناً تحتاج التحكم بسلوك زر الرجوع — مثلاً في صفحة الدفع حيث تريد عرض تأكيد قبل المغادرة. Shell يمنحك هذه المرونة:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyShopApp.Views.CheckoutPage"
Title="الدفع"
Shell.NavBarIsVisible="True"
Shell.TabBarIsVisible="False">
<!-- تخصيص زر الرجوع -->
<Shell.BackButtonBehavior>
<BackButtonBehavior Command="{Binding BackCommand}"
IconOverride="arrow_back.png"
TextOverride="رجوع" />
</Shell.BackButtonBehavior>
<!-- محتوى الصفحة -->
<VerticalStackLayout Padding="20">
<Label Text="مراجعة الطلب" FontSize="24" />
</VerticalStackLayout>
</ContentPage>
الربط العميق (Deep Linking) على Android وiOS
الربط العميق يسمح بفتح صفحة محددة في تطبيقك مباشرة من رابط خارجي — سواء من بريد إلكتروني أو إشعار أو حتى QR code. وهنا يتألق Shell حقاً، لأنه يعمل كجسر طبيعي لهذه العملية.
إعداد Android App Links
على Android، تحتاج إعداد Intent Filter في ملف MainActivity.cs:
[Activity(Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Android.Content.Intent.ActionView },
Categories = new[]
{
Android.Content.Intent.CategoryDefault,
Android.Content.Intent.CategoryBrowsable
},
DataScheme = "https",
DataHost = "myshopapp.com",
DataPathPrefix = "/products",
AutoVerify = true)]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnNewIntent(Android.Content.Intent intent)
{
base.OnNewIntent(intent);
if (intent?.Data != null)
{
HandleDeepLink(intent.Data.ToString());
}
}
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
if (Intent?.Data != null)
{
HandleDeepLink(Intent.Data.ToString());
}
}
private async void HandleDeepLink(string url)
{
var uri = new Uri(url);
if (uri.AbsolutePath.StartsWith("/products/"))
{
var productId = uri.AbsolutePath.Split('/').Last();
await Shell.Current.GoToAsync(
$"productdetails?id={productId}");
}
}
}
ستحتاج أيضاً لاستضافة ملف assetlinks.json على موقعك للتحقق من ملكية النطاق:
// https://myshopapp.com/.well-known/assetlinks.json
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.myshopapp.maui",
"sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
}
}]
إعداد iOS Universal Links
على iOS، الموضوع مختلف قليلاً. تحتاج إعداد Associated Domains وملف Apple App Site Association.
أولاً، أضف الصلاحية في ملف المشروع .csproj:
<ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
<CustomEntitlements
Include="com.apple.developer.associated-domains"
Type="StringArray"
Value="applinks:myshopapp.com?mode=developer" />
</ItemGroup>
ثم أضف معالج دورة الحياة في MauiProgram.cs:
builder.ConfigureLifecycleEvents(events =>
{
#if IOS
events.AddiOS(ios => ios
.ContinueUserActivity((app, userActivity, handler) =>
{
if (userActivity.ActivityType == Foundation.NSUserActivityType.BrowsingWeb
&& userActivity.WebPageUrl != null)
{
HandleDeepLink(userActivity.WebPageUrl.ToString());
}
return true;
}));
#endif
});
static async void HandleDeepLink(string url)
{
var uri = new Uri(url);
if (uri.AbsolutePath.StartsWith("/products/"))
{
var productId = uri.AbsolutePath.Split('/').Last();
await Shell.Current.GoToAsync($"productdetails?id={productId}");
}
}
وأخيراً، استضف ملف apple-app-site-association على موقعك:
// https://myshopapp.com/.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [{
"appID": "TEAM_ID.com.myshopapp.maui",
"paths": ["/products/*", "/orders/*"]
}]
}
}
أفضل الممارسات لنظام التنقل في .NET MAUI 10
بعد التجربة والعمل مع Shell لفترة طويلة، هذه أهم النصائح التي أتمنى لو عرفتها من البداية:
- استخدم IQueryAttributable دائماً: بدلاً من QueryPropertyAttribute — لضمان التوافق مع NativeAOT والتقليم الكامل. هذا ليس اختيارياً في المشاريع الجديدة
- فضّل ShellNavigationQueryParameters: بدلاً من Dictionary العادي لتمرير البيانات — لتجنب مشكلة "البيانات القديمة" عند الرجوع للخلف
- سمّ مساراتك بوضوح: استخدم أسماء وصفية مثل
productdetailsبدلاً منpage2. مستقبلك سيشكرك - لا تنسَ await مع GoToAsync: عدم انتظار العملية قد يسبب سلوكاً غير متوقع وأحياناً crashes عشوائية
- استخدم المسارات المطلقة (//) للتنقل الجذري: عندما تريد إعادة تعيين المكدس بالكامل — مثل بعد تسجيل الدخول أو الخروج
- أنشئ خدمة تنقل: لا تستدعِ Shell.Current مباشرة من ViewModels. الكود يصبح أسهل في الاختبار والصيانة
- اختبر الربط العميق على أجهزة حقيقية: المحاكيات لا تدعم دائماً التحقق من النطاق بشكل صحيح (خاصة على iOS)
- تعامل مع حالات الخطأ: إذا وصل رابط عميق بمعرّف غير صالح، وجّه المستخدم إلى صفحة بديلة بدلاً من تركه يرى شاشة فارغة
الأسئلة الشائعة
ما الفرق بين Shell وNavigationPage في .NET MAUI؟
Shell هو النظام الحديث والمُوصى به. يوفر تنقلاً مبنياً على URI مع دعم مدمج للقائمة الجانبية والتبويبات والبحث. أما NavigationPage فهو أبسط ويعتمد على مكدس Push/Pop — مناسب للتدفقات الخطية فقط. ملاحظة مهمة: لا يمكن استخدامهما معاً في نفس التطبيق.
كيف أمرر كائناً كاملاً بين الصفحات في .NET MAUI؟
أفضل طريقة هي استخدام Dictionary<string, object> أو ShellNavigationQueryParameters مع GoToAsync، ثم تطبيق واجهة IQueryAttributable على ViewModel المُستقبِل. هذه الطريقة آمنة مع NativeAOT ومتوافقة مع MVVM.
هل يدعم .NET MAUI الربط العميق (Deep Linking)؟
نعم. يدعم .NET MAUI كلاً من Android App Links وiOS Universal Links. تحتاج إعداد Intent Filter على Android وAssociated Domains على iOS، مع استضافة ملفات التحقق على موقعك. Shell يعمل كجسر لتوجيه الروابط إلى الصفحات المناسبة عبر GoToAsync.
لماذا لا يُنصح باستخدام QueryPropertyAttribute في .NET MAUI 10؟
لأنه يعتمد على الانعكاس (Reflection) في وقت التشغيل، وهو غير آمن مع التقليم الكامل وNativeAOT اللذين يزيلان الكود غير المُستخدم أثناء البناء. استخدم IQueryAttributable بدلاً منه — هذا هو البديل الرسمي من Microsoft.
كيف أتعامل مع مشكلة "البيانات القديمة" عند الرجوع للخلف في Shell؟
عند استخدام Dictionary العادي، تبقى البيانات في الذاكرة وتُعاد إلى الصفحة عند الرجوع إليها. الحل الأسهل هو استخدام ShellNavigationQueryParameters التي تُمسح تلقائياً بعد الاستخدام الأول. أو يمكنك التحقق يدوياً في ApplyQueryAttributes مما إذا كانت البيانات جديدة.