In this video we perform a full step by step build of a .NET MAUI App that we test on both Windows and Android. The app interacts with a separate .NET 6 API that we also build step by step.
Level: Beginner
⏲️ Time Codes ⏲️
Theory
- 0:48 Welcome
- 03:13 App demo
- 06:07 Course overview
- 09:14 Ingedients
- 10:10 What is .NET MAUI?
- 12:48 How MAUI works
- 15:14 MAUI project anatomy
- 19:47 MAUI App start up sequence
- 22:29 UI Conepts
- 28:21 XAML vs C#
- 30:29 Solution Architecture
- 31:41 Application Architecture
API Build
- 35:31 API Project Set up
- 42:41 API Model definition
- 44:47 API Db Context
- 47:13 Connection String
- 52:19 Migrations
- 56:31 API Read Endpoint
- 1:01:58 API Create Endpoint
- 1:08:15 API Update Endpoint
- 1:12:57 API Delete Endpoint
MAUI App Build
- 1:17:21 MAUI App Project Set up
- 1:21:00 Android Device Manager
- 1:25:08 MAUI Model definition
- 1:31:16 Data Service Interface
- 1:35:40 Data Service Implementation
- 1:47:27 Data Service Read Method
- 1:53:34 Data Service Create Method
- 1:58:48 Data Service Delete Method
- 2:01:53 Data Service Update Method
- 2:05:41 Android environment config
- 2:11:00 Architecture check point
- 2:11:54 Register MainPage for DI
- 2:14:13 MainPage code-behind
- 2:21:03 MainPage XAML Layout
- 2:30:19 Re-work MainPage layout
- 2:35:12 Add another page (ManagePage)
- 2:38:01 Adding a Route
- 2:30:01 Regiter ManagePage for DI
- 2:40:29 Complete MainPage code-behind
- 2:45:12 ManagePage code-behind
- 2:51:16 QueryProperty
- 2:57:34 ManagePage XMAL
- 3:07:56 Run on Windows
- 3:09:30 Re-work ManagePage layout
- 3:16:26 Using HttpClientFactory
Outro
- 3:21:02 Wrap up and thanks
- 3:21:31 Supporter Credits
تفاوت بین متدهای app.Use و app.Run در چیست؟
Middlewareها به همان ترتیبی که در متد Configure کلاس آغازین برنامه معرفی میشوند، اجرا خواهند شد؛ اما نکتهی مهم اینجا است که middleware ایی که توسط متد app.Use تعریف میشود، میتواند middleware بعدی ثبت شدهرا، فراخوانی کند؛ اما app.Run خیر. برای درک بهتر این مفهوم، به مثال زیر دقت کنید:
using Microsoft.AspNetCore.Http; public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { await context.Response.WriteAsync("<div>from middleware-1, inside app.Use, before next()</div>"); await next(); await context.Response.WriteAsync("<div>from middleware-1, inside app.Use, after next()</div>"); }); app.Run(async context => { await context.Response.WriteAsync("<div>Inside middleware-2 defined using app.Run</div>"); }); app.Use(async (context, next) => { await context.Response.WriteAsync("<div>from middleware-3, inside app.Use, before next()</div>"); await next(); await context.Response.WriteAsync("<div>from middleware-3, inside app.Use, after next()</div>"); });
همانطور که در تصویر نیز مشخص است، ابتدا کدهای پیش از فراخوانی دلیگیت next میانافزار اول اجرا شدهاست. سپس باتوجه به فراخوانی دلیگیت next، کدهای دومین میانافزار ثبت شده، فراخوانی گردیدهاست و سپس کدهای پس از فراخوانی دلیگیت next میانافزار اول، اجرا شدهاند.
این دلیگیت در اصل یک چنین امضایی را دارد:
public delegate Task RequestDelegate(HttpContext context);
باید دقت داشت که فراخوانی دلیگیت next در میانافزارهای از نوع app.Use الزامی نبوده و اگر اینکار انجام نشود، بین app.Run و app.Use تفاوتی نخواهد بود و هر دو terminal به حساب میآیند.
تفاوت بین متدهایapp.Map و app.MapWhen در چیست؟
متد app.Map در صورت برآورده شدن شرطی، سبب اجرای میانافزاری مشخص میشود (امکان اجرای غیر خطی میانافزارها).
فرض کنید قطعه کد زیر را پس از اولین app.Use مثال فوق قرار دادهایم:
app.Map("/dnt", appBuilder => { appBuilder.Run(async context => { await context.Response.WriteAsync(@"<div>Inside Map(/dnt) --> Run</div>"); }); });
در اینجا چون app.Run داخلی فراخوانی شده، از نوع terminal است، دیگر میان افزارهای پس از آن اجرا نشدهاند. بدیهی است در اینجا نیز میتوان به هر تعدادی که نیاز است میان افزارهای جدیدی را به appBuilder متد app.Map اضافه کرد.
پارامتر اول متد Map برای تطابق با الگوهایی خاص و مشخص، مناسب است. اما در اگر در اینجا نیاز به اطلاعات بیشتری از HttpContext جاری داشته باشیم، میتوانیم از متد app.MapWhen استفاده کنیم که اولین پارامتر آن یک دلیگیت است که HttpContext را در اختیار استفاده کننده قرار میدهد و اگر در نهایت true را دریافت کند، سبب اجرای میان افزارهای قسمت appBuilder آن خواهد شد:
app.MapWhen(context => { return context.Request.Query.ContainsKey("dnt"); }, appBuilder => { appBuilder.Run(async context => { await context.Response.WriteAsync(@"<div>Inside MapWhen(?dnt) --> Run</div>"); }); });
http://localhost:7742/?dnt=true
نظم بخشیدن به تعاریف میانافزارها
متدهای app.Run و app.Use و امثال آنها برای تعریف سریع میان افزارها مناسب هستند. اما اگر بخواهیم کدهای کلاس آغازین برنامه را اندکی خلوت کرده و به تعاریف میانافزارها نظم ببخشیم، میتوان کدهای آنها را به کلاسهایی با امضایی خاص منتقل کرد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Core1RtmEmptyTest.StartupCustomizations { public class MyMiddleware1 { private readonly RequestDelegate _next; public MyMiddleware1(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { context.Response.ContentType = "text/html"; context.Response.StatusCode = 200; await context.Response.WriteAsync("<div>Hello from MyMiddleware1.</div>"); await _next.Invoke(context); await context.Response.WriteAsync("<div>End of action.</div>"); } } }
در اینجا نیز اگر دلیگیت next_ فراخوانی نشود، این میانافزار سبب خاتمهی اجرای پردازش درخواست جاری میگردد.
مرحلهی بعد، روش معرفی این میانافزار تعریف شده، به لیست میانافزارهای موجود است. برای این منظور میتوان متد app.UseMiddleware را به صورت مستقیم در کلاس آغازین برنامه فراخوانی کرد و یا مرسوم است اگر کتابخانهای را طراحی کردهاید، به نحو ذیل متد الحاقی خاصی را برای آن تدارک دید:
using Microsoft.AspNetCore.Builder; public static class MyMiddlewareExtensions { public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app) { app.UseMiddleware<MyMiddleware1>(); return app; } }
public void Configure(IApplicationBuilder app) { app.UseMyMiddleware();
Microsoft has announced that the .NET Core 2.0 will be considered "end of life" and thus no longer supported as of October 1, 2018. .NET Core 2.0 is considered a non-LTS release, and as such Microsoft only commits its support for three months after a successor has been released. In this case, with .NET Core 2.1 having been released May 31, .NET Core 2.0’s end has come.
در ،قسمت قبلی پیاده سازی درختها را بررسی کردیم و در این قسمت مبحث گرافها را آغاز میکنیم .
گرافها یکی از پر اهمیتترین و
همچنین پر استفادهترین ساختارها هستند و به خوبی روابط بین تمامی اشیاء را نشان میدهند
و در عمل تقریبا همه چیز را پشتیبانی میکنند. در ادامه متوجه خواهید شد که درختها،
زیر مجموعهای از گرافها هستند و همانطور که میدانید لیستها هم زیر مجموعهی درختها
هستند. پس لیستها هم زیر مجموعهی گرافها میشوند .
مفهوم پایهای گراف
در این بخش تعدادی از مهمترین خصوصیات
گرافها را بررسی میکنیم که تعدادی از آنها بسیار شبیه به ساختمان درختهاست، ولی
تفاوتهای زیادی با هم دارند؛ زیرا که درخت، خود نوع متفاوتی از ساختمان گرافها است .
درخت زیر را در نظر بگیرید؛ این درخت هم مانند سایر درختها با گرههای شماره دار، نامگذاری شده است که تشخیص آنها را برای
ما آسانتر میسازد. در گراف، به گرهها راس یا vertice هم
میگویند. هر چند نام گره هم برای آنها به کار برده میشود. به فلشهایی که به
این رئوس اشاره میکنند، لبههای جهت دار directed edges گفته میشود که در ویکی پدیا و کتب آموزشی فارسی، به آنها یال
اطلاق میشود . به یال هایی که از هر راس بیرون میآیند Predecessor گفته میشود که به معنی آغاز کننده است و به راسی که اشاره میکند، Successor گویند که
به معنی ارث برنده یا جایگزین شناخته میشود. در شکل زیر عدد راس 19 آغاز کننده راس
1 است و 1 هم جایگزین یا ارث برنده 19 و اگر با دقت به شکل نگاه کنید میبینید که
یک راس میتواند از چند راس ارث برنده باشد؛ مثل راس 21 .
برای نمایش گراف، ما از عبارت (V,E) استفاده میکنیم که
V مجموعهای از راسها و E مجموعهای از لبههاست. هر لبه (که با
e کوچک نمایش داده میشود) و در مجموعه E قرار دارد، پیوند دو راس
v و
u را نشان میدهد یا به عبارتی به صورت
ریاضی میشود (e=(u,v .
برای
اینکه مطالب را بهتر درک کنیم
بهتر است که هر راس را یک شهر و هر لبه را یک جادهی یک طرفه برای ارتباط با
این راسها فرض کنیم. مثلا اگر یکی از راسها را تهران تصور کنیم و دیگری را
کاشان، لبه یا
جادهی یک طرفهای که این دو شهر را به هم متصل میکند، میشود جاده یا لبهی تهران کاشان.
در بعضی مواقع از لبههای بدون جهت استفاده میشود که این لبهها را لبههای دو طرفه میگویند؛ مثل جادهی دو طرفه. گاهی هم از دو لبهی جهت دار به جای یک لبهی بدون جهت استفاده میکنند که نمونهی آن را در شکل زیر میبینید.
دو راسی که به وسیلهی یک یال به یکدیگر متصل میشوند را همسایه Neighbor مینامند. هر یال میتواند یک عدد برای خود داشته باشد که به این عدد وزن یال یا لبه میگویندWeight (Cost) و در مثال بالا میتوان گفت وزن هر یال میشود مسافت آن جاده؛ مسافتی که بین دو شهر همسایه باید طی شود. تصویر زیر یک گراف را نشان میدهد که وزن یالهای آن در کنار هر یال نوشته شده است.
مسیر Path در گراف همانند درختها، طی کردن مسیری است که از یک راس به راس دیگر میرسد. در مثال بالا باید گفت که برای رسیدن از شهر مبدا به شهر مقصد، باید از چه شهرهایی عبور کرد. در شکل بالا مسیر 1 - 12 - 19 - 21 - 7 - 21 و 1 مسیر نیستند؛ چرا که راس 21 هیچ لبهی آغاز کنندهای ندارد و بیشتر ارث برنده است.
طول Length هر مسیر هم تعداد یالهایی است که در طول مسیر قرار دارد یا تعداد راسها منهای یک؛ به این مثال دقت کنید:
مسیر 1 -12-19-21 مسیری است که طول آن سه میباشد.
وزن مسیر هم از جمع وزن یالهایی که در طول مسیر طی میشود به دست میآید.
حلقه Loop مسیری است که راس اولیه با راس نهایی یکی باشد. نمونهی آن مسیر 1-12-19-1 میباشد. ولی مسیر 1-7-21 حلقهای تشکیل نمیدهد.
لبهی حلقه ای Looping Edge لبهای است که مبداء یا آغاز کنندهی آن با مقصد یا ارث برندهی آن یکی باشد. یعنی به راسی وصل شود که از همان، آغاز شده است. مثل لبهی متصل به راس 14.
یک کلاس گراف به چه مواردی نیاز دارد:
عملیات ایجاد گراف
افزودن و حذف یک راس یا لبه
بررسی اینکه بین دو راس لبه ای وجود دارد یا خیر
جست و جوی جانشینهای یک راس
در قسمت آینده کد آن را در سی شارپ پیاده سازی خواهیم کرد.
var roles = ((ClaimsIdentity)User.Identity).Claims .Where(c => c.Type == ClaimTypes.Role) .Select(c => c.Value);
من پروژه شما رو دانلود کردم و یک کلاس ساده در بخش ApplicationDbContext اضافه کردم جهت تست.
اما بعد از اجرای پروژه چنین کلاسی در دیتابیس ایجاد نگردید.
با ردیابی کلیات کار این قطعه کد رو در یک پروژه جدید ( با داشتن بخش Identity core) تست کردم که نتایج همانند قبل بود و تنها جداول Identity ایجاد گردید.
try { using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) { var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); db.Database.Migrate(); } } catch (Exception ex) { //log error }
آیا برای ردیابی تغییرات مدل و اعمال به دیتابیس به صورت اتوماتیک نیاز به کار خاص یا موردی هست که جامونده باشه ؟
بررسی مفهوم AppDomain
معرفی فایل جدید ViewImports
پروژهی خالی ASP.NET Core 1.0 فاقد پوشهی Views به همراه فایلهای آغازین آن است. بنابراین ابتدا در ریشهی پروژه، پوشهی جدید Views را ایجاد کنید.
فایلهای آغازین این پوشه هم در مقایسهی با نگارشهای قبلی ASP.NET MVC اندکی تغییر کردهاند. برای مثال در نگارش قبلی، فایل web.config ایی در ریشهی پوشهی Views قرار داشت و چندین مقصود را فراهم میکرد:
الف) در آن تنظیم شده بود که هر نوع درخواستی به فایلهای موجود در پوشهی Views، برگشت خورده و قابل پردازش نباشند. این مورد هم از لحاظ مسایل امنیتی اضافه شده بود و هم اینکه در ASP.NET MVC، برخلاف وب فرمها، شروع پردازش یک درخواست، از فایلهای View شروع نمیشود. به همین جهت است که درخواست مستقیم آنها بیمعنا است.
در ASP.NET Core، فایل web.config از این پوشه حذف شدهاست؛ چون دیگر نیازی به آن نیست. اگر مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایلهای استاتیک» را به خاطر داشته باشید، هر پوشهای که توسط میان افزار Static Files عمومی نشود، توسط کاربران برنامه قابل دسترسی نخواهد بود و چون پوشهی Views هم به صورت پیش فرض توسط این میان افزار عمومی نمیشود، نیازی به فایل web.config، جهت قطع دسترسی به فایلهای موجود در آن وجود ندارد.
ب) کاربرد دیگر این فایل web.config، تعریف فضاهای نام پیش فرضی بود که در فایلهای View مورد استفاده قرار میگرفتند. برای مثال چون فضای نام HTML Helperهای استاندارد ASP.NET MVC در این فایل web.config قید شده بود، دیگر نیازی به تکرار آن در تمام فایلهای View برنامه وجود نداشت. در ASP.NET Core، برای جایگزین کردن این قابلیت، فایل جدیدی را به نام ViewImports.cshtml_ معرفی کردهاند، تا دیگر نیازی به ارث بری از فایل web.config وجود نداشته باشد.
برای مثال اگر میخواهید بالای Viewهای خود، مدام ذکر using مربوط به فضای نام مدلها برنامه را انجام ندهید، این سطر تکراری را به فایل جدید view imports منتقل کنید:
@using MyProject.Models
و این فضاهای نام به صورت پیش فرض برای تمام viewها مهیا هستند و نیازی به تعریف مجدد، ندارند:
• System
• System.Linq
• System.Collections.Generic
• Microsoft.AspNetCore.Mvc
• Microsoft.AspNetCore.Mvc.Rendering
افزودن یک View جدید
در نگارشهای پیشین ASP.NET MVC، اگر بر روی نام یک اکشن متد کلیک راست میکردیم، در منوی ظاهر شده، گزینهی Add view وجود داشت. چنین گزینهای در نگارش RTM اول ASP.NET Core وجود ندارد و مراحل ایجاد یک View جدید را باید دستی طی کنید. برای مثال اگر نام کلاس کنترلر شما PersonController است، پوشهی Person را به عنوان زیر پوشهی Views ایجاد کرده و سپس بر روی این پوشه کلیک راست کنید، گزینهی add new item را انتخاب و سپس واژهی view را جستجو کنید:
البته یک دلیل این مساله میتواند امکان سفارشی سازی محل قرارگیری این پوشهها در ASP.NET Core نیز باشد که در ادامه آنرا بررسی خواهیم کرد (و ابزارهای از پیش تعریف شده عموما با مکانهای از پیش تعریف شده کار میکنند).
امکان پوشه بندی بهتر فایلهای یک پروژهی ASP.NET Core نسبت به مفهوم Areas در نگارشهای پیشین ASP.NET MVC
حالت پیش فرض پوشه بندی فایلهای اصلی برنامههای ASP.NET MVC، مبتنی بر فناوریها است؛ برای مثال پوشههای views و Controllers و امثال آن تعریف شدهاند.
Project - Controllers - Models - Services - ViewModels - Views
هرکسی که مدتی با ASP.NET MVC کار کرده باشد حتما به این مشکل برخوردهاست. درحال پیاده سازی قابلیتی هستید و برای اینکار نیاز خواهید داشت مدام بین پوشههای مختلف برنامه سوئیچ کنید؛ از پوشهی کنترلرها به پوشهی ویووها، به پوشهی اسکریپتها، پوشهی اشتراکی ویووها و غیره. پس از رشد برنامه به جایی خواهید رسید که دیگر نمیتوانید تشخیص دهید این فایلی که اضافه شدهاست ارتباطش با سایر قسمتها چیست؟
ایدهی «پوشه بندی بر اساس ویژگیها»، بر مبنای قرار دادن تمام نیازهای یک ویژگی، درون یک پوشهی خاص آن است:
همانطور که مشاهده میکنید، در این حالت تمام اجزای یک ویژگی، داخل یک پوشه قرار گرفتهاند؛ از کنترلر مرتبط با Viewهای آن تا فایلهای css و js خاص آن.
برای پیاده سازی آن:
1) نام پوشهی Views را به Features تغییر دهید.
2) پوشهای را به نام StartupCustomizations به برنامه اضافه کرده و سپس کلاس ذیل را به آن اضافه کنید:
using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Razor; namespace Core1RtmEmptyTest.StartupCustomizations { public class FeatureLocationExpander : IViewLocationExpander { public void PopulateValues(ViewLocationExpanderContext context) { context.Values["customviewlocation"] = nameof(FeatureLocationExpander); } public IEnumerable<string> ExpandViewLocations( ViewLocationExpanderContext context, IEnumerable<string> viewLocations) { return new[] { "/Features/{1}/{0}.cshtml", "/Features/Shared/{0}.cshtml" }; } } }
RazorViewEngine برنامه، بر اساس وهلهی پیش فرضی از اینترفیس IViewLocationExpander، محل یافتن Viewها را دریافت میکند. با استفاده از پیاه سازی فوق، این پیش فرضها را به پوشهی features هدایت کردهایم.
3) در ادامه به کلاس آغازین برنامه مراجعه کرده و پس از فعال سازی ASP.NET MVC، این قابلیت را فعال سازی میکنیم:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.Configure<RazorViewEngineOptions>(options => { options.ViewLocationExpanders.Add(new FeatureLocationExpander()); });
5) اکنون باید پوشهی Controllers خالی شده باشد. این پوشه را کلا حذف کنید. از این جهت که کنترلرها بر اساس پیش فرضهای ASP.NET MVC (کلاس ختم شدهی به کلمهی Controller واقع در اسمبلی که از وابستگیهای ASP.NET MVC استفاده میکند) در هر مکانی که تعریف شده باشند، یافت خواهند شد و پوشهی واقع شدن آنها مهم نیست.
6) در آخر به فایل project.json مراجعه کرده و قسمت publish آنرا جهت درج نام پوشهی Features اصلاح کنید (تا در حین توزیع نهایی استفاده شود):
"publishOptions": { "include": [ "wwwroot", "Features", "appsettings.json", "web.config" ] },
در اینجا نیز یک نمونهی دیگر استفادهی از این روش بسیار معروف را مشاهده میکنید.
امکان ارائهی برنامه بدون ارائهی فایلهای View آن
ASP.NET Core به همراه یک EmbeddedFileProvider نیز هست. حالت پیش فرض آن PhysicalFileProvider است که بر اساس تنظیمات IViewLocationExpander توکار (و یا نمونهی سفارشی فوق در مبحث پوشهی ویژگیها) کار میکند.
برای راه اندازی آن ابتدا نیاز است بستهی نیوگت ذیل را به فایل project.json اضافه کرد:
{ "dependencies": { //same as before "Microsoft.Extensions.FileProviders.Embedded": "1.0.0" },
services.AddMvc(); services.Configure<RazorViewEngineOptions>(options => { options.ViewLocationExpanders.Add(new FeatureLocationExpander()); var thisAssembly = typeof(Startup).GetTypeInfo().Assembly; options.FileProviders.Clear(); options.FileProviders.Add(new EmbeddedFileProvider(thisAssembly, baseNamespace: "Core1RtmEmptyTest")); });
"buildOptions": { "emitEntryPoint": true, "preserveCompilationContext": true, "embed": "Features/**/*.cshtml" },
1) اگر نام پوشهی Views را به Features تغییر دادهاید، نیاز به ثبت ViewLocationExpanders آنرا دارید (وگرنه، خیر).
2) در اینجا جهت مثال و بررسی اینکه واقعا این فایلها از اسمبلی برنامه خوانده میشوند، متد options.FileProviders.Clear فراخوانی شدهاست. این متد PhysicalFileProvider پیش فرض را حذف میکند. کار PhysicalFileProvider خواندن فایلهای ویووها از فایل سیستم به صورت متداول است.
3) کار قسمت embed در تنظیمات build، افزودن فایلهای cshtml به قسمت منابع اسمبلی است (به همین جهت دیگر نیازی به توزیع آنها نخواهد بود). اگر صرفا **/Features را ذکر کنید، تمام فایلهای موجود را پیوست میکند. همچنین اگر نام پوشهی Views را تغییر ندادهاید، این مقدار همان Views/**/*.cshtml خواهد بود و یا **/Views
4) در EmbeddedFileProvider میتوان هر نوع اسمبلی را ذکر کرد. یعنی میتوان برنامه را به صورت چندین و چند ماژول تهیه و سپس سرهم و یکپارچه کرد (options.FileProviders یک لیست قابل تکمیل است). در اینجا ذکر baseNamespace نیز مهم است. در غیر اینصورت منبع مورد نظر از اسمبلی یاد شده، قابل استخراج نخواهد بود (چون نام اسمبلی، قسمت اول نام هر منبعی است).
فعال سازی کامپایل خودکار فایلهای View در ASP.NET Core 1.0
این قابلیت به زودی جهت یافتن مشکلات موجود در فایلهای razor پیش از اجرای آنها، اضافه خواهد شد. اطلاعات بیشتر