اشتراکها
9 ویژگی جدید SQL Server 2017
با سلام و تشکر؛ لطفا در مورد اجرای پروژههای دات نت کور بر روی سرویس دهندههای ریموت هم توضیح بفرمایید.فرض میکنیم سورس یک پروژه وب دات نت کور وجود دارد و قرار است بر روی کانتینرهای ریموت (مثلا ابر آروان یا سرویس دهندههای خارجی) اجر شود. آیا امکان دارد که مستقیم سورس پروژه روی داکر ریموت (نه Docker Desktop ) بیلد و اجرا شود؟
یه مدت روی اوبونتو از vsCode استفاده کردم. خیلی خوبه و سبک و راضی کننده ست. منتها مشکل اصلی من دیباگ کردن پروژههای docker base بود. راه حل هایی توی نت بود. منتها ساده نبودن و به همین خاطر فعلا بیخیال شدم و روی ویندوز با 2017 vs کار میکنم. اگر بشه به راحتی از داکر استفاده کرد برمیگردم روی vsCode
به روز رسانی خودکار برنامه توسط خود آن (self-update) با بررسی اطلاعات آخرین به روز رسانی، از وبسایت ما.
کاربر نگارشهای جدید دریافتی را (از طریق ایمیل و یا وب سایت ما) بر روی نگارشهای فعلی کپی میکند.
کاربر یک برنامهی به روز رسانی نصاب مانند را اجرا میکند.
کاربران به علت پیچیدگی برنامه نمیتوانند برنامه را به روز رسانی کنند؛ ما برای آنها اینکار را انجام میدهیم.
از ClickOnce استفاده میکنیم.
توسط سیستم به روز رسانی App Storeها.
از Docker استفاده میکنیم.
کاربر نگارشهای جدید دریافتی را (از طریق ایمیل و یا وب سایت ما) بر روی نگارشهای فعلی کپی میکند.
کاربر یک برنامهی به روز رسانی نصاب مانند را اجرا میکند.
کاربران به علت پیچیدگی برنامه نمیتوانند برنامه را به روز رسانی کنند؛ ما برای آنها اینکار را انجام میدهیم.
از ClickOnce استفاده میکنیم.
توسط سیستم به روز رسانی App Storeها.
از Docker استفاده میکنیم.
پس از آشنایی با دو مدل هاستینگ برنامههای Blazor در قسمت قبل، اکنون میخواهیم ساختار ابتدایی قالبهای این دو پروژه را بررسی کنیم.
ایجاد پروژههای خالی Blazor
در انتهای قسمت قبل، با روش ایجاد پروژههای خالی Blazor به کمک NET SDK 5x. آشنا شدیم. به همین جهت دو پوشهی جدید BlazorWasmSample و BlazorServerSample را ایجاد کرده و از طریق خط فرمان و با کمک NET CLI.، در پوشهی اولی دستور dotnet new blazorwasm و در پوشهی دومی دستور dotnet new blazorserver را اجرا میکنیم.
البته اجرای این دو دستور، نیاز به اتصال به اینترنت را هم برای بار اول دارند؛ تا فایلهای مورد نیاز و بستههای مرتبط را دریافت و restore کنند. بسته به سرعت اینترنت، حداقل یک ربعی را باید صبر کنید تا دریافت ابتدایی بستههای مرتبط به پایان برسد. برای دفعات بعدی، از کش محلی NuGet، برای restore بستههای blazor استفاده میشود که بسیار سریع است.
بررسی ساختار پروژهی Blazor Server و اجرای آن
پس از اجرای دستور dotnet new blazorserver در یک پوشهی خالی و ایجاد پروژهی ابتدایی آن:
همانطور که مشاهده میکنید، ساختار این پروژه، بسیار شبیه به یک پروژهی استاندارد ASP.NET Core از نوع Razor pages است.
- در پوشهی properties آن، فایل launchSettings.json قرار دارد که برای نمونه، شماره پورت اجرایی برنامه را در حالت اجرای توسط دستور dotnet run و یا توسط IIS Express مشخص میکند.
- پوشهی wwwroot آن، مخصوص ارائهی فایلهای ایستا مانند wwwroot\css\bootstrap است. در ابتدای کار، این پوشه به همراه فایلهای CSS بوت استرپ است. در ادامه اگر نیاز باشد، فایلهای جاوا اسکریپتی را نیز میتوان به این قسمت اضافه کرد.
- در پوشهی Data آن، سرویس تامین اطلاعاتی اتفاقی قرار دارد؛ به نام WeatherForecastService که هدف آن، تامین اطلاعات یک جدول نمایشی است که در ادامه در آدرس https://localhost:5001/fetchdata قابل مشاهده است.
- در پوشهی Pages، تمام کامپوننتهای Razor برنامه قرار میگیرند. یکی از مهمترین صفحات آن، فایل Pages\_Host.cshtml است. کار این صفحهی ریشه، افزودن تمام فایلهای CSS و JS، به برنامهاست. بنابراین در آینده نیز از همین صفحه برای افزودن فایلهای CSS و JS استفاده خواهیم کرد. اگر به قسمت body این صفحه دقت کنیم، تگ جدید کامپوننت قابل مشاهدهاست:
کار آن، رندر کامپوننت App.razor واقع در ریشهی پروژهاست.
همچنین در همینجا، تگهای دیگری نیز قابل مشاهده هستند:
همانطور که در قسمت قبل نیز عنوان شد، اتصال برنامهی در حال اجرای در مرورگر blazor server با backend، از طریق یک اتصال دائم SignalR صورت میگیرد. اگر در این بین خطای اتصالی رخ دهد، div با id مساوی blazor-error-ui فوق، یکی از پیامهایی را که مشاهده میکنید، بسته به اینکه برنامه در حالت توسعه در حال اجرا است و یا در حالت ارائهی نهایی، نمایش میدهد. افزودن مدخل blazor.server.js نیز به همین منظور است. کار آن مدیریت اتصال دائم SignalR، به صورت خودکار است و از این لحاظ نیازی به کدنویسی خاصی از سمت ما ندارد. این اتصال، کار به روز رسانی UI و هدایت رخدادها را به سمت سرور، انجام میدهد.
- در پوشهی Shared، یکسری فایلهای اشتراکی قرار دارند که قرار است در کامپوننتهای واقع در پوشهی Pages مورد استفاه قرار گیرند. برای نمونه فایل Shared\MainLayout.razor، شبیه به master page برنامههای web forms است که قالب کلی سایت را مشخص میکند. داخل آن Body@ را مشاهده میکنید که به معنای نمایش صفحات دیگر، دقیقا در همین محل است. همچنین در این پوشه فایل Shared\NavMenu.razor نیز قرار دارد که ارجاعی به آن در MainLayout.razor ذکر شده و کار آن نمایش منوی آبیرنگ سمت چپ صفحهاست.
- در پوشهی ریشهی برنامه، فایل Imports.razor_ قابل مشاهدهاست. مزیت تعریف usingها در اینجا این است که از تکرار آنها در کامپوننتهای razor ای که در ادامه تهیه خواهیم کرد، جلوگیری میکند. هر using تعریف شدهی در اینجا، در تمام کامپوننتها، قابل دسترسی است؛ به آن global imports هم گفته میشود.
- در پوشهی ریشهی برنامه، فایل App.razor نیز قابل مشاهدهاست. کار آن تعریف قالب پیشفرض برنامهاست که برای مثال به Shared\MainLayout.razor اشاره میکند. همچنین کامپوننت مسیریابی نیز در اینجا ذکر شدهاست:
اگر شخصی مسیر از پیش تعریف شدهای را وارد کند، به قسمت Found وارد میشود و در غیر اینصورت به قسمت NotFound. در قسمت Found است که از قالب MainLayout برای رندر یک کامپوننت توسط کامپوننت RouteView، استفاده خواهد شد و در قسمت NotFound، فقط پیام «Sorry, there's nothing at this address» به کاربر نمایش داده میشود. قالبهای هر دو نیز قابل تغییر و سفارشی سازی هستند.
- فایل appsettings.json نیز همانند برنامههای استاندارد ASP.NET Core در اینجا مشاهده میشود.
- فایل Program.cs آن که نقطهی آغازین برنامهاست و کار فراخوانی Startup.cs را انجام میدهد، دقیقا با یک فایل Program.cs برنامهی استاندارد ASP.NET Core یکی است.
- در فایل Startup.cs آن، همانند قبل دو متد تنظیم سرویسها و تنظیم میانافزارها قابل مشاهدهاست.
در ConfigureServices آن، سرویسهای صفحات razor و ServerSideBlazor اضافه شدهاند. همچنین سرویس نمونهی WeatherForecastService نیز در اینجا ثبت شدهاست.
قسمتهای جدید متد Configure آن، ثبت مسیریابی توکار BlazorHub است که مرتبط است با اتصال دائم SignalR برنامه و اگر مسیری پیدا نشد، به Host_ هدایت میشود:
در ادامه در همان پوشه، دستور dotnet run را اجرا میکنیم تا پروژه، کامپایل و همچنین وب سرور آن نیز اجرا شده و برنامه در آدرس https://localhost:5001 قابل دسترسی شود.
که به همراه 13 درخواست و نزدیک به 600 KB دریافت اطلاعات از سمت سرور است.
این برنامهی نمونه، به همراه سه صفحهی نمایش Home، نمایش یک شمارشگر و نمایش اطلاعاتی از پیش آماده شدهاست. اگر صفحهی شمارشگر آنرا باز کنیم، با کلیک بر روی دکمهی آن، هرچند مقدار current count افزایش مییابد، اما شاهد post-back متداولی به سمت سرور نیستیم و این صفحه بسیار شبیه به صفحات برنامههای SPA (تک صفحهای وب) به نظر میرسد:
همانطور که عنوان شد، مدخل blazor.server.js فایل Pages\_Host.cshtml، کار به روز رسانی UI و هدایت رخدادها را به سمت سرور به صورت خودکار انجام میدهد. به همین جهت است که post-back ای را مشاهده نمیکنیم و برنامه، شبیه به یک برنامهی SPA به نظر میرسد؛ هر چند تمام رندرهای آن سمت سرور انجام میشوند و توسط SignalR به سمت کلاینت بازگشت داده خواهند شد.
برای نمونه اگر بر روی دکمهی شمارشگر کلیک کنیم، در برگهی network مرورگر، هیچ اثری از آن مشاهده نمیشود (هیچ رفت و برگشتی را مشاهده نمیکنیم). علت اینجا است که اطلاعات متناظر با این کلیک، از طریق web socket باز شدهی توسط SignalR، به سمت سرور ارسال شده و نتیجهی واکنش به این کلیکها و رندر HTML نهایی سمت سرور آن، از همین طریق به سمت کلاینت بازگشت داده میشود.
بررسی ساختار پروژهی Blazor WASM و اجرای آن
پس از اجرای دستور dotnet new blazorwasm در یک پوشهی خالی و ایجاد پروژهی ابتدایی آن:
همان صفحات پروژهی خالی Blazor Server در اینجا نیز قابل مشاهده هستند. این برنامهی نمونه، به همراه سه صفحهی نمایش Home، نمایش یک شمارشگر و نمایش اطلاعاتی از پیش آماده شدهاست. صفحات و کامپوننتهای پوشههای Pages و Shared نیز دقیقا همانند پروژهی Blazor Server قابل مشاهده هستند. مفاهیمی مانند فایلهای Imports.razor_ و App.razor نیز مانند قبل هستند.
البته در اینجا فایل Startup ای مشاهده نمیشود و تمام تنظیمات آغازین برنامه، داخل فایل Program.cs انجام خواهند شد:
در اینجا ابتدا کامپوننت App.razor را به برنامه معرفی میکند که ساختار آن با نمونهی مشابه Blazor Server دقیقا یکی است. سپس سرویسی را برای دسترسی به HttpClient، به سیستم تزریق وابستگیهای برنامه معرفی میکند. هدف از آن، دسترسی سادهتر به endpointهای یک ASP.NET Core Web API است. از این جهت که در یک برنامهی سمت کلاینت، دیگر دسترسی مستقیمی به سرویسهای سمت سرور را نداریم و برای کار با آنها همانند سایر برنامههای SPA که از Ajax استفاده میکنند، در اینجا از HttpClient برای کار با Web APIهای مختلف استفاده میشود.
تفاوت ساختاری دیگر این پروژهی WASM، با نمونهی Blazor Server، ساختار پوشهی wwwroot آن است:
که به همراه فایل جدید نمونهی wwwroot\sample-data\weather.json است؛ بجای سرویس متناظر سمت سرور آن در برنامهی blazor server. همچنین فایل جدید wwwroot\index.html نیز قابل مشاهدهاست و محتوای تگ body آن به صورت زیر است:
- چون این برنامه، یک برنامهی سمت کلاینت است، اینبار بجای فایل Host_ سمت سرور، فایل index.html سمت کلاینت را برای ارائهی آغازین برنامه داریم که وابستگی به دات نت ندارد و توسط مرورگر قابل درک است.
- در ابتدای بارگذاری برنامه، یک loading نمایش داده میشود که در اینجا نحوهی تعریف آن مشخص است. همچنین اگر خطایی رخ دهد نیز توسط div ای با id مساوی blazor-error-ui اطلاع رسانی میشود.
- مدخل blazor.webassembly.js در اینجا، کار دریافت وب اسمبلی و فایلهای NET runtime. را انجام میدهد؛ برخلاف برنامههای Blazor Server که توسط فایل blazor.server.js، یک ارتباط دائم SignalR را با سرور برقرار میکردند تا کدهای رندر شدهی سمت سرور را دریافت و نمایش دهند و یا اطلاعاتی را به سمت سرور ارسال کنند: برای مثال بر روی دکمهای کلیک شدهاست، اطلاعات مربوطه را در سمت سرور پردازش کن و نتیجهی نهایی رندر شده را بازگشت بده. اما در اینجا همه چیز داخل مرورگر اجرا میشود و برای این نوع اعمال، رفت و برگشتی به سمت سرور صورت نمیگیرد. به همین جهت تمام کدهای #C ما به سمت کلاینت ارسال شده و داخل مرورگر به کمک فناوری وب اسمبلی، اجرا میشوند. در اینجا از لحاظ ارسال تمام کدهای مرتبط با UI برنامهی سمت کلاینت به مرورگر کاربر، تفاوتی با فریمورکهایی مانند Angular و یا React نیست و آنها هم تمام کدهای UI برنامه را کامپایل کرده و یکجا ارسال میکنند.
در ادامه در همان پوشه، دستور dotnet run را اجرا میکنیم تا پروژه کامپایل و همچنین وب سرور آن نیز اجرا شده و برنامه در آدرس https://localhost:5001 قابل دسترسی شود.
که به همراه 205 درخواست و نزدیک به 9.6 MB دریافت اطلاعات از سمت سرور است. البته اگر همین صفحه را refresh کنیم، دیگر شاهد دریافت مجدد فایلهای DLL مربوط به NET Runtime. نخواهیم بود و اینبار از کش مرورگر خوانده میشوند:
در این برنامهی سمت کلاینت، ابتدا تمام فایلهای NET Runtime. و وب اسمبلی دریافت شده و سپس اجرای تغییرات UI، در همین سمت و بدون نیاز به اتصال دائم SignalR ای به سمت سرور، پردازش و نمایش داده میشوند. به همین جهت زمانیکه بر روی دکمهی شمارشگر آن کلیک میکنیم، اتفاقی در برگهی network مرورگر ثبت نمیشود و رفت و برگشتی به سمت سرور صورت نمیگیرد.
عدم وجود اتصال SignalR، مزیت امکان اجرای آفلاین برنامهی WASM را نیز میسر میکند. برای مثال یکبار دیگر همان برنامهی Blazor Server را به کمک دستور dotnet run اجرا کنید. سپس آنرا در مرورگر در آدرس https://localhost:5001 باز کنید. اکنون پنجرهی کنسولی که dotnet run را اجرا کرده، خاتمه دهید (قسمت اجرای سمت سرور آنرا ببندید).
بلافاصله تصویر «سعی در اتصال مجدد» فوق را مشاهده خواهیم کرد که به دلیل قطع اتصال SignalR رخ دادهاست. یعنی یک برنامهی Blazor Server، بدون این اتصال دائم، قادر به ادامهی فعالیت نیست. اما چنین محدودیتی با برنامههای Blazor WASM وجود ندارد.
البته بدیهی است اگر یک Web API سمت سرور برای ارائهی اطلاعاتی به برنامهی WASM نیاز باشد، API سمت سرور برنامه نیز باید جهت کار و نمایش اطلاعات در سمت کلاینت در دسترس باشد؛ وگرنه قسمتهای متناظر با آن، قادر به نمایش و یا ثبت اطلاعات نخواهند بود.
ایجاد پروژههای خالی Blazor
در انتهای قسمت قبل، با روش ایجاد پروژههای خالی Blazor به کمک NET SDK 5x. آشنا شدیم. به همین جهت دو پوشهی جدید BlazorWasmSample و BlazorServerSample را ایجاد کرده و از طریق خط فرمان و با کمک NET CLI.، در پوشهی اولی دستور dotnet new blazorwasm و در پوشهی دومی دستور dotnet new blazorserver را اجرا میکنیم.
البته اجرای این دو دستور، نیاز به اتصال به اینترنت را هم برای بار اول دارند؛ تا فایلهای مورد نیاز و بستههای مرتبط را دریافت و restore کنند. بسته به سرعت اینترنت، حداقل یک ربعی را باید صبر کنید تا دریافت ابتدایی بستههای مرتبط به پایان برسد. برای دفعات بعدی، از کش محلی NuGet، برای restore بستههای blazor استفاده میشود که بسیار سریع است.
بررسی ساختار پروژهی Blazor Server و اجرای آن
پس از اجرای دستور dotnet new blazorserver در یک پوشهی خالی و ایجاد پروژهی ابتدایی آن:
همانطور که مشاهده میکنید، ساختار این پروژه، بسیار شبیه به یک پروژهی استاندارد ASP.NET Core از نوع Razor pages است.
- در پوشهی properties آن، فایل launchSettings.json قرار دارد که برای نمونه، شماره پورت اجرایی برنامه را در حالت اجرای توسط دستور dotnet run و یا توسط IIS Express مشخص میکند.
- پوشهی wwwroot آن، مخصوص ارائهی فایلهای ایستا مانند wwwroot\css\bootstrap است. در ابتدای کار، این پوشه به همراه فایلهای CSS بوت استرپ است. در ادامه اگر نیاز باشد، فایلهای جاوا اسکریپتی را نیز میتوان به این قسمت اضافه کرد.
- در پوشهی Data آن، سرویس تامین اطلاعاتی اتفاقی قرار دارد؛ به نام WeatherForecastService که هدف آن، تامین اطلاعات یک جدول نمایشی است که در ادامه در آدرس https://localhost:5001/fetchdata قابل مشاهده است.
- در پوشهی Pages، تمام کامپوننتهای Razor برنامه قرار میگیرند. یکی از مهمترین صفحات آن، فایل Pages\_Host.cshtml است. کار این صفحهی ریشه، افزودن تمام فایلهای CSS و JS، به برنامهاست. بنابراین در آینده نیز از همین صفحه برای افزودن فایلهای CSS و JS استفاده خواهیم کرد. اگر به قسمت body این صفحه دقت کنیم، تگ جدید کامپوننت قابل مشاهدهاست:
<body> <component type="typeof(App)" render-mode="ServerPrerendered" />
همچنین در همینجا، تگهای دیگری نیز قابل مشاهده هستند:
<body> <component type="typeof(App)" render-mode="ServerPrerendered" /> <div id="blazor-error-ui"> <environment include="Staging,Production"> An error has occurred. This application may no longer respond until reloaded. </environment> <environment include="Development"> An unhandled exception has occurred. See browser dev tools for details. </environment> <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div> <script src="_framework/blazor.server.js"></script> </body>
- در پوشهی Shared، یکسری فایلهای اشتراکی قرار دارند که قرار است در کامپوننتهای واقع در پوشهی Pages مورد استفاه قرار گیرند. برای نمونه فایل Shared\MainLayout.razor، شبیه به master page برنامههای web forms است که قالب کلی سایت را مشخص میکند. داخل آن Body@ را مشاهده میکنید که به معنای نمایش صفحات دیگر، دقیقا در همین محل است. همچنین در این پوشه فایل Shared\NavMenu.razor نیز قرار دارد که ارجاعی به آن در MainLayout.razor ذکر شده و کار آن نمایش منوی آبیرنگ سمت چپ صفحهاست.
- در پوشهی ریشهی برنامه، فایل Imports.razor_ قابل مشاهدهاست. مزیت تعریف usingها در اینجا این است که از تکرار آنها در کامپوننتهای razor ای که در ادامه تهیه خواهیم کرد، جلوگیری میکند. هر using تعریف شدهی در اینجا، در تمام کامپوننتها، قابل دسترسی است؛ به آن global imports هم گفته میشود.
- در پوشهی ریشهی برنامه، فایل App.razor نیز قابل مشاهدهاست. کار آن تعریف قالب پیشفرض برنامهاست که برای مثال به Shared\MainLayout.razor اشاره میکند. همچنین کامپوننت مسیریابی نیز در اینجا ذکر شدهاست:
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router>
- فایل appsettings.json نیز همانند برنامههای استاندارد ASP.NET Core در اینجا مشاهده میشود.
- فایل Program.cs آن که نقطهی آغازین برنامهاست و کار فراخوانی Startup.cs را انجام میدهد، دقیقا با یک فایل Program.cs برنامهی استاندارد ASP.NET Core یکی است.
- در فایل Startup.cs آن، همانند قبل دو متد تنظیم سرویسها و تنظیم میانافزارها قابل مشاهدهاست.
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton<WeatherForecastService>(); }
قسمتهای جدید متد Configure آن، ثبت مسیریابی توکار BlazorHub است که مرتبط است با اتصال دائم SignalR برنامه و اگر مسیری پیدا نشد، به Host_ هدایت میشود:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); }); }
که به همراه 13 درخواست و نزدیک به 600 KB دریافت اطلاعات از سمت سرور است.
این برنامهی نمونه، به همراه سه صفحهی نمایش Home، نمایش یک شمارشگر و نمایش اطلاعاتی از پیش آماده شدهاست. اگر صفحهی شمارشگر آنرا باز کنیم، با کلیک بر روی دکمهی آن، هرچند مقدار current count افزایش مییابد، اما شاهد post-back متداولی به سمت سرور نیستیم و این صفحه بسیار شبیه به صفحات برنامههای SPA (تک صفحهای وب) به نظر میرسد:
همانطور که عنوان شد، مدخل blazor.server.js فایل Pages\_Host.cshtml، کار به روز رسانی UI و هدایت رخدادها را به سمت سرور به صورت خودکار انجام میدهد. به همین جهت است که post-back ای را مشاهده نمیکنیم و برنامه، شبیه به یک برنامهی SPA به نظر میرسد؛ هر چند تمام رندرهای آن سمت سرور انجام میشوند و توسط SignalR به سمت کلاینت بازگشت داده خواهند شد.
برای نمونه اگر بر روی دکمهی شمارشگر کلیک کنیم، در برگهی network مرورگر، هیچ اثری از آن مشاهده نمیشود (هیچ رفت و برگشتی را مشاهده نمیکنیم). علت اینجا است که اطلاعات متناظر با این کلیک، از طریق web socket باز شدهی توسط SignalR، به سمت سرور ارسال شده و نتیجهی واکنش به این کلیکها و رندر HTML نهایی سمت سرور آن، از همین طریق به سمت کلاینت بازگشت داده میشود.
بررسی ساختار پروژهی Blazor WASM و اجرای آن
پس از اجرای دستور dotnet new blazorwasm در یک پوشهی خالی و ایجاد پروژهی ابتدایی آن:
همان صفحات پروژهی خالی Blazor Server در اینجا نیز قابل مشاهده هستند. این برنامهی نمونه، به همراه سه صفحهی نمایش Home، نمایش یک شمارشگر و نمایش اطلاعاتی از پیش آماده شدهاست. صفحات و کامپوننتهای پوشههای Pages و Shared نیز دقیقا همانند پروژهی Blazor Server قابل مشاهده هستند. مفاهیمی مانند فایلهای Imports.razor_ و App.razor نیز مانند قبل هستند.
البته در اینجا فایل Startup ای مشاهده نمیشود و تمام تنظیمات آغازین برنامه، داخل فایل Program.cs انجام خواهند شد:
namespace BlazorWasmSample { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); await builder.Build().RunAsync(); } } }
تفاوت ساختاری دیگر این پروژهی WASM، با نمونهی Blazor Server، ساختار پوشهی wwwroot آن است:
که به همراه فایل جدید نمونهی wwwroot\sample-data\weather.json است؛ بجای سرویس متناظر سمت سرور آن در برنامهی blazor server. همچنین فایل جدید wwwroot\index.html نیز قابل مشاهدهاست و محتوای تگ body آن به صورت زیر است:
<body> <div id="app">Loading...</div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div> <script src="_framework/blazor.webassembly.js"></script> </body>
- در ابتدای بارگذاری برنامه، یک loading نمایش داده میشود که در اینجا نحوهی تعریف آن مشخص است. همچنین اگر خطایی رخ دهد نیز توسط div ای با id مساوی blazor-error-ui اطلاع رسانی میشود.
- مدخل blazor.webassembly.js در اینجا، کار دریافت وب اسمبلی و فایلهای NET runtime. را انجام میدهد؛ برخلاف برنامههای Blazor Server که توسط فایل blazor.server.js، یک ارتباط دائم SignalR را با سرور برقرار میکردند تا کدهای رندر شدهی سمت سرور را دریافت و نمایش دهند و یا اطلاعاتی را به سمت سرور ارسال کنند: برای مثال بر روی دکمهای کلیک شدهاست، اطلاعات مربوطه را در سمت سرور پردازش کن و نتیجهی نهایی رندر شده را بازگشت بده. اما در اینجا همه چیز داخل مرورگر اجرا میشود و برای این نوع اعمال، رفت و برگشتی به سمت سرور صورت نمیگیرد. به همین جهت تمام کدهای #C ما به سمت کلاینت ارسال شده و داخل مرورگر به کمک فناوری وب اسمبلی، اجرا میشوند. در اینجا از لحاظ ارسال تمام کدهای مرتبط با UI برنامهی سمت کلاینت به مرورگر کاربر، تفاوتی با فریمورکهایی مانند Angular و یا React نیست و آنها هم تمام کدهای UI برنامه را کامپایل کرده و یکجا ارسال میکنند.
در ادامه در همان پوشه، دستور dotnet run را اجرا میکنیم تا پروژه کامپایل و همچنین وب سرور آن نیز اجرا شده و برنامه در آدرس https://localhost:5001 قابل دسترسی شود.
که به همراه 205 درخواست و نزدیک به 9.6 MB دریافت اطلاعات از سمت سرور است. البته اگر همین صفحه را refresh کنیم، دیگر شاهد دریافت مجدد فایلهای DLL مربوط به NET Runtime. نخواهیم بود و اینبار از کش مرورگر خوانده میشوند:
در این برنامهی سمت کلاینت، ابتدا تمام فایلهای NET Runtime. و وب اسمبلی دریافت شده و سپس اجرای تغییرات UI، در همین سمت و بدون نیاز به اتصال دائم SignalR ای به سمت سرور، پردازش و نمایش داده میشوند. به همین جهت زمانیکه بر روی دکمهی شمارشگر آن کلیک میکنیم، اتفاقی در برگهی network مرورگر ثبت نمیشود و رفت و برگشتی به سمت سرور صورت نمیگیرد.
عدم وجود اتصال SignalR، مزیت امکان اجرای آفلاین برنامهی WASM را نیز میسر میکند. برای مثال یکبار دیگر همان برنامهی Blazor Server را به کمک دستور dotnet run اجرا کنید. سپس آنرا در مرورگر در آدرس https://localhost:5001 باز کنید. اکنون پنجرهی کنسولی که dotnet run را اجرا کرده، خاتمه دهید (قسمت اجرای سمت سرور آنرا ببندید).
بلافاصله تصویر «سعی در اتصال مجدد» فوق را مشاهده خواهیم کرد که به دلیل قطع اتصال SignalR رخ دادهاست. یعنی یک برنامهی Blazor Server، بدون این اتصال دائم، قادر به ادامهی فعالیت نیست. اما چنین محدودیتی با برنامههای Blazor WASM وجود ندارد.
البته بدیهی است اگر یک Web API سمت سرور برای ارائهی اطلاعاتی به برنامهی WASM نیاز باشد، API سمت سرور برنامه نیز باید جهت کار و نمایش اطلاعات در سمت کلاینت در دسترس باشد؛ وگرنه قسمتهای متناظر با آن، قادر به نمایش و یا ثبت اطلاعات نخواهند بود.
چند روز پیش در یکی از سیستمهای مدیریتی که امکان آپلود چند تصویر وجود داشت، مسئلهای پیش آمد که نیاز بود تصاویر وارد شده، اندازهای به یک نسبت را داشته باشند و کاربران مرتبا تصاویری با اندازههای مختلف را وارد میکردند که باعث میشد UI در حین نمایش تصاویر، از شکل اصلی خود دور شود. به همین دلیل امکان برش تصاویر یا Crop برای این امر احساس میشد. کتابخانههای مختلف و زیادی برای برش تصاویر و کار بر روی تصاویر وجود دارند، ولی این کتابخانه باید چند خصوصیت زیر را دارا باشد:
یک. قبل از آپلود امکان برش وجود داشته باشد.
دو. سیستم برش قابلیت پشتیبانی از تناسب بین پهنا و ارتفاع را داشته باشد.
سه. قابلیت استفاده خارج از قواعد Ajax را داشته باشد و بتوان ارسال آن را به طور دستی کنترل کرد.
چهار. پشتیبانی برش تصاویر به صورت تاچ برای گوشیهای همراه را نیز دارا باشد.
کتابخانه jcrop کتابخانهای است که این امکانات را برای شما فراهم میکند. این کتابخانه بدین صورت است که در حین برش به شما 4 عدد x1,x2,y1,y2 را داده و شما با ارسال آن به سمت سرور میتوانید بر اساس این اعداد، عکس اصلی را برش بزنید. بدین صورت شما هم عکس اصلی را دارید و هم مختصات برش را دارید و اگر دوست دارید در جاهای مختلف از عکس اصلی برش داشته باشید، بسیار مفید خواهد بود.
مرحله اول:
ابتدا فایل jcrop را دانلود نمایید.
مرحله دوم:
کد Html زیر را به صفحه اضافه کنید:
در اینجا 4 عدد input به صورت مخفی قرار گرفتهاند که مختصات برش به ترتیب در آنها ذخیره میشوند و هنگام post یا قرار دادن در formData جهت ارسال ایجکسی نیز میتوانند مورد استفاده قرار بگیرند. جهت input file مورد نظر برای زیبایی و پشتیبانی از عبارت فارسی دلخواه به جای «Browse» از کتابخانه Bootstrap FileStyle استفاده شده است و رویداد Onchange آن نیز به یک تابع جاوا اسکریپتی بایند شده تا بعد از تایید تصویر، باقی عملیات برش تصویر انجام شوند.
تگ step2 نیز بعد از نمایش موفقیت آمیز تصویر نشان داده میشود که کاربر میتواند در آن تصویر را برش دهد و شامل بخش info نیز میباشد تا بتوان اندازه اصلی تصویر، نوع فایل تصویر Content Type و حجم آن را نمایش داد.
مرحله سوم:
سپس برای استایل دهی کدهای بالا از کد Css زیر استفاده میکنیم:
مرحله چهارم:
افزودن کد جاوااسکریپتی زیر برای کار کردن با کتابخانه Jcrop میباشد:
تابع fileSelectHandler
این تابع که مستقیما به input file رویداد Onchange متصل است. فایل انتخابی آن را خوانده و نوع تصویر را بررسی میکند. اگر تصویر از نوع Jpeg یا Png نباشد، یک خطا را به کاربر نشان میدهد و با return شدن از ادامه کد جلوگیری میکنیم.
در ادامه همین تابع بالا، کدهای زیر را اضافه میکنیم:
FileReader یکی از توابع موجود در HTML است که مستندات آن در سایت موزیلا موجود است و قابلیت خواندن غیرهمزمان فایلها و اشیا Blob را دارد. در خط آخر به عنوان پارامتر ما فایلی را که در آپلودر خوانده ایم و در مرحله قبل نوع فایل آن را بررسی کردیم، پاس میکنیم و باعث میشود که رویداد Load شیء FileReader صدا زده شود.
در این رویداد ابتدا اطلاعات این فایل را از قبیل سایز و ابعاد و نوع فایل، خوانده و در همان تگ Div که با کلاس info تعیین شده بود، نمایش میدهیم. سپس متغیر jcrop_api را که به صورت global در بالای تابع صدا زدیم، بررسی میکنم که آیا از قبل پر شدهاست یا خیر؟ اگر از قبل پرشدهاست باید شیء Jcrop را که به آن اعمال شده است، نابود و آن را نال کنیم تا برای تصویر جدید آماده شود. این کد زمانی کاربرد دارد که کاربر از تصویر قبلی انصراف دادهاست و تصویر جدیدی را انتخاب نموده است یا اینکه عملیات دارد به صورت ایجکسی پیاده میشود. اگر عملیات نابودی روی این پلاگین صورت نگیرد، برای مرتبه دوم کار نخواهد کرد.
سپس پلاگین جیکوئری Jcrop را بر روی آن اعمال میکنیم. در پرامتر اول یک سری تنظیمات اولیه را انجام میدهیم که در ادامه با آن آشنا میشویم و در پارامتر دوم یک callback را به آن پاس میکنیم تا بعد از آماده شدن پلاگین اجرا شود که در آن شیء جدید ایجاد شده یعنی this را در متغیری به اسم jcrop_api دخیره میکنیم تا در بررسیهای آتی که در بند بالا توضیح داده شد، در دسترس داشته باشیم. همچنین در این تابع شما میتوانید اندازه تصویر انتخابی را نیز داشته باشید.
این پلاگین شامل optionهای متفاوتی در پارامتر اول است که آنها را بررسی میکنیم:
MinSize : شما میتوانید حداقل پهنا و ارتفاعی را برای برش زدن تصویر در نظر بگیرید.
پارامتر دوم aspectRatio جهت نگهداری تناسب بین پهنا و ارتفاع میباشد. تنظیم زیر برای تناسب 3 به دو میباشد.
bgFade اگر با true مقدار دهی شود عملیات برش در حین تاریک شدن صفحه (محلی که جزء برش نیست) و یا بالعکس آن با یک Fade صورت خواهد گرفت.
bgOpacity از 0 تا یک مقدار میگیرد و میزان opacity محلهای تاریک را تعیین میکند. همچنین شامل سه رویداد onSelect,onChange,onrelease هم میباشد که به ترتیب در موارد زیر رخ میدهند:
ناحیه مورد نظر انتخاب شد.
ناحیه مورد نظر در حالت انتخاب است و ماوس در حال درگ شدن است و با هر حرکتی ماوس اجرا میگردد.
ناحیه انتخابی از حالت انتخاب خارج شد.
دو رویداد اول یعنی onchange و onSelect را برای به روزسانی فیلدهای مخفی و مختصات استفاده میکنیم:
این مختصات از طریق یک پارامتر به آنها پاس میشود. به غیر از این چهار عدد مختصات میتوانید با استفاده از متغیرهای w و h هم اندازه پهنا و ارتفاع محل برش خورده را نیز به دست آورید. هر چند که این اعداد، از تفریق خود مختصات هم به دست میآیند.
یک تابع جزئی دیگر هم در این فایل وجود دارد که حین نمایش اندازه تصویر، واحد نمایش مناسب آن را برای ما انتخاب میکند:
بعد از اینکه کدهای سمت کلاینت را تمام کردیم لازم است با نحوه برش تصویر در سمت سرور هم آشنا شویم:
از آنجا که ما تصاویر را در دیتابیس به صورت آرایهای از بایتها ذخیره میکنیم، extension method ذکر شده در بالا تصویر را در حالت آرایهای از بایتها برش میدهد. بدیهی که بسته به نیاز شما کد بالا دست خوش تغییراتی خواهد شد. ابتدا تصویر باینری را به شی Image تبدیل میکنیم و یک شیء Bitmap جدید را به عنوان بوم خالی و به اندازه کادر برش ایجاد میکنیم تا تصویر برش خورده در آن قرار بگیرد و سپس توسط متد DrawImage میخواهیم که تصویر مبدا را با مختصات شیء Rectangle از نقطه 0 و 0 بوم آغاز کرده و تا انتهای آن شروع به ترسیم کند. سپس آن را ذخیره و مجددا در قالب همان آرایهای از بایتها بر میگردانیم.
تنها یک نکته را به خاطر داشته باشید که مقادیر مختصاتی که پلاگین جی کوئری ارسال میکند در قالب اعداد اعشاری هستند و برای ارسال و دریافت آنها در سرور این نکته را به خاطر داشته باشید.
یک. قبل از آپلود امکان برش وجود داشته باشد.
دو. سیستم برش قابلیت پشتیبانی از تناسب بین پهنا و ارتفاع را داشته باشد.
سه. قابلیت استفاده خارج از قواعد Ajax را داشته باشد و بتوان ارسال آن را به طور دستی کنترل کرد.
چهار. پشتیبانی برش تصاویر به صورت تاچ برای گوشیهای همراه را نیز دارا باشد.
کتابخانه jcrop کتابخانهای است که این امکانات را برای شما فراهم میکند. این کتابخانه بدین صورت است که در حین برش به شما 4 عدد x1,x2,y1,y2 را داده و شما با ارسال آن به سمت سرور میتوانید بر اساس این اعداد، عکس اصلی را برش بزنید. بدین صورت شما هم عکس اصلی را دارید و هم مختصات برش را دارید و اگر دوست دارید در جاهای مختلف از عکس اصلی برش داشته باشید، بسیار مفید خواهد بود.
مرحله اول:
ابتدا فایل jcrop را دانلود نمایید.
مرحله دوم:
کد Html زیر را به صفحه اضافه کنید:
<div> <!-- upload form --> <!-- hidden crop params --> <input type="hidden" id="x1" name="x1" /> <input type="hidden" id="y1" name="y1" /> <input type="hidden" id="x2" name="x2" /> <input type="hidden" id="y2" name="y2" /> <h2>ابتدا تصویر خود را انتخاب کنید</h2> <div><input type="file" name="postedFileBase" data-buttonText="انتخاب تصویر" id="image_file" onchange="fileSelectHandler()" /></div> <div></div> <div> <h2>قسمتی از تصویر را انتخاب نمایید</h2> <img id="preview" /> <div> <label>حجم فایل </label> <input type="text" id="filesize" name="filesize" /> <label>نوع فایل</label> <input type="text" id="filetype" name="filetype" /> <label>ابعاد فایل</label> <input style="direction: ltr;" type="text" id="filedim" name="filedim" /> </div> </div> </div>
تگ step2 نیز بعد از نمایش موفقیت آمیز تصویر نشان داده میشود که کاربر میتواند در آن تصویر را برش دهد و شامل بخش info نیز میباشد تا بتوان اندازه اصلی تصویر، نوع فایل تصویر Content Type و حجم آن را نمایش داد.
مرحله سوم:
سپس برای استایل دهی کدهای بالا از کد Css زیر استفاده میکنیم:
.bheader { background-color: #DDDDDD; border-radius: 10px 10px 0 0; padding: 10px 0; text-align: center; } .bbody { color: #000; overflow: hidden; padding-bottom: 20px; text-align: center; background: -moz-linear-gradient(#ffffff, #f2f2f2); background: -ms-linear-gradient(#ffffff, #f2f2f2); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f2f2f2)); background: -webkit-linear-gradient(#ffffff, #f2f2f2); background: -o-linear-gradient(#ffffff, #f2f2f2); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f2f2f2'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f2f2f2')"; background: linear-gradient(#ffffff, #f2f2f2); } .bbody h2, .info, .error { margin: 10px 0; } .step2, .error { display: none; } .error { color: red; } .info { } label { margin: 0 5px; } .roundinput { border: 1px solid #CCCCCC; border-radius: 10px; padding: 4px 8px; text-align: center; width: 150px; } .jcrop-holder { display: inline-block; } input[type=submit] { background: #e3e3e3; border: 1px solid #bbb; border-radius: 3px; -webkit-box-shadow: inset 0 0 1px 1px #f6f6f6; box-shadow: inset 0 0 1px 1px #f6f6f6; color: #333; padding: 8px 0 9px; text-align: center; text-shadow: 0 1px 0 #fff; width: 150px; } input[type=submit]:hover { background: #d9d9d9; -webkit-box-shadow: inset 0 0 1px 1px #eaeaea; box-shadow: inset 0 0 1px 1px #eaeaea; color: #222; cursor: pointer; } input[type=submit]:active { background: #d0d0d0; -webkit-box-shadow: inset 0 0 1px 1px #e3e3e3; box-shadow: inset 0 0 1px 1px #e3e3e3; color: #000; }
مرحله چهارم:
افزودن کد جاوااسکریپتی زیر برای کار کردن با کتابخانه Jcrop میباشد:
function bytesToSize(bytes) { var sizes = ['بایت', 'کیلو بایت', 'مگابایت']; if (bytes == 0) return 'n/a'; var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i]; }; function updateInfo(e) { $('#x1').val(e.x); $('#y1').val(e.y); $('#x2').val(e.x2); $('#y2').val(e.y2); }; var jcrop_api, boundx, boundy; function fileSelectHandler() { var oFile = $('#image_file')[0].files[0]; $('.error').hide(); var rFilter = /^(image\/jpeg|image\/png)$/i; if (!rFilter.test(oFile.type)) { $('.error').html('فقط تصویر معتبر انتخاب نمایید').show(); return; } var oImage = document.getElementById('preview'); var oReader = new FileReader(); oReader.onload = function (e) { oImage.src = e.target.result; oImage.onload = function () { $('.step2').fadeIn(500); var sResultFileSize = bytesToSize(oFile.size); $('#filesize').val(sResultFileSize); $('#filetype').val(oFile.type); $('#filedim').val(oImage.naturalWidth + ' x ' + oImage.naturalHeight); if (typeof jcrop_api != 'undefined') { jcrop_api.destroy(); jcrop_api = null; $('#preview').width(oImage.naturalWidth); $('#preview').height(oImage.naturalHeight); } $('#preview').Jcrop({ aspectRatio: 2, bgFade: true, bgOpacity: .3, onChange: updateInfo, onSelect: updateInfo }, function () { //var bounds = this.getBounds(); //var boundx = bounds[0]; //var boundy = bounds[1]; // Store the Jcrop API in the jcrop_api variable jcrop_api = this; }); }; }; oReader.readAsDataURL(oFile); }
تابع fileSelectHandler
function fileSelectHandler() { var oFile = $('#image_file')[0].files[0]; $('.error').hide(); var rFilter = /^(image\/jpeg|image\/png)$/i; if (!rFilter.test(oFile.type)) { $('.error').html('فقط تصویر معتبر انتخاب نمایید').show(); return; }
در ادامه همین تابع بالا، کدهای زیر را اضافه میکنیم:
var oImage = document.getElementById('preview'); var oReader = new FileReader(); oReader.onload = function (e) { oImage.src = e.target.result; oImage.onload = function () { $('.step2').fadeIn(500); var sResultFileSize = bytesToSize(oFile.size); $('#filesize').val(sResultFileSize); $('#filetype').val(oFile.type); $('#filedim').val(oImage.naturalWidth + ' x ' + oImage.naturalHeight); if (typeof jcrop_api != 'undefined') { jcrop_api.destroy(); jcrop_api = null; $('#preview').width(oImage.naturalWidth); $('#preview').height(oImage.naturalHeight); } $('#preview').Jcrop({ aspectRatio: 2, bgFade: true, bgOpacity: .3, onChange: updateInfo, onSelect: updateInfo, onRelease: clearInfo }, function () { //var bounds = this.getBounds(); //var boundx = bounds[0]; //var boundy = bounds[1]; jcrop_api = this; }); }; }; oReader.readAsDataURL(oFile);
FileReader یکی از توابع موجود در HTML است که مستندات آن در سایت موزیلا موجود است و قابلیت خواندن غیرهمزمان فایلها و اشیا Blob را دارد. در خط آخر به عنوان پارامتر ما فایلی را که در آپلودر خوانده ایم و در مرحله قبل نوع فایل آن را بررسی کردیم، پاس میکنیم و باعث میشود که رویداد Load شیء FileReader صدا زده شود.
در این رویداد ابتدا اطلاعات این فایل را از قبیل سایز و ابعاد و نوع فایل، خوانده و در همان تگ Div که با کلاس info تعیین شده بود، نمایش میدهیم. سپس متغیر jcrop_api را که به صورت global در بالای تابع صدا زدیم، بررسی میکنم که آیا از قبل پر شدهاست یا خیر؟ اگر از قبل پرشدهاست باید شیء Jcrop را که به آن اعمال شده است، نابود و آن را نال کنیم تا برای تصویر جدید آماده شود. این کد زمانی کاربرد دارد که کاربر از تصویر قبلی انصراف دادهاست و تصویر جدیدی را انتخاب نموده است یا اینکه عملیات دارد به صورت ایجکسی پیاده میشود. اگر عملیات نابودی روی این پلاگین صورت نگیرد، برای مرتبه دوم کار نخواهد کرد.
سپس پلاگین جیکوئری Jcrop را بر روی آن اعمال میکنیم. در پرامتر اول یک سری تنظیمات اولیه را انجام میدهیم که در ادامه با آن آشنا میشویم و در پارامتر دوم یک callback را به آن پاس میکنیم تا بعد از آماده شدن پلاگین اجرا شود که در آن شیء جدید ایجاد شده یعنی this را در متغیری به اسم jcrop_api دخیره میکنیم تا در بررسیهای آتی که در بند بالا توضیح داده شد، در دسترس داشته باشیم. همچنین در این تابع شما میتوانید اندازه تصویر انتخابی را نیز داشته باشید.
این پلاگین شامل optionهای متفاوتی در پارامتر اول است که آنها را بررسی میکنیم:
MinSize : شما میتوانید حداقل پهنا و ارتفاعی را برای برش زدن تصویر در نظر بگیرید.
minSize:[40,20]
aspectRatio:1.5
bgOpacity از 0 تا یک مقدار میگیرد و میزان opacity محلهای تاریک را تعیین میکند. همچنین شامل سه رویداد onSelect,onChange,onrelease هم میباشد که به ترتیب در موارد زیر رخ میدهند:
ناحیه مورد نظر انتخاب شد.
ناحیه مورد نظر در حالت انتخاب است و ماوس در حال درگ شدن است و با هر حرکتی ماوس اجرا میگردد.
ناحیه انتخابی از حالت انتخاب خارج شد.
دو رویداد اول یعنی onchange و onSelect را برای به روزسانی فیلدهای مخفی و مختصات استفاده میکنیم:
function updateInfo(e) { $('#x1').val(e.x); $('#y1').val(e.y); $('#x2').val(e.x2); $('#y2').val(e.y2); };
این مختصات از طریق یک پارامتر به آنها پاس میشود. به غیر از این چهار عدد مختصات میتوانید با استفاده از متغیرهای w و h هم اندازه پهنا و ارتفاع محل برش خورده را نیز به دست آورید. هر چند که این اعداد، از تفریق خود مختصات هم به دست میآیند.
یک تابع جزئی دیگر هم در این فایل وجود دارد که حین نمایش اندازه تصویر، واحد نمایش مناسب آن را برای ما انتخاب میکند:
function bytesToSize(bytes) { var sizes = ['بایت', 'کیلو بایت', 'مگابایت']; if (bytes == 0) return 'n/a'; var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i]; };
بعد از اینکه کدهای سمت کلاینت را تمام کردیم لازم است با نحوه برش تصویر در سمت سرور هم آشنا شویم:
public static byte[] Resize(this byte[] byteImageIn, int x1,int y1,int x2,int y2) { ImageConverter ic = new ImageConverter(); Image src = (Image)(ic.ConvertFrom(byteImageIn)); Bitmap target = new Bitmap(x2 - x1, y2 - y1); using (Graphics graphics = Graphics.FromImage(target)) graphics.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height), new Rectangle(x1,y1,x2-x1,y2-y1), GraphicsUnit.Pixel); src = target; using (var ms = new MemoryStream()) { src.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); return ms.ToArray(); } }
از آنجا که ما تصاویر را در دیتابیس به صورت آرایهای از بایتها ذخیره میکنیم، extension method ذکر شده در بالا تصویر را در حالت آرایهای از بایتها برش میدهد. بدیهی که بسته به نیاز شما کد بالا دست خوش تغییراتی خواهد شد. ابتدا تصویر باینری را به شی Image تبدیل میکنیم و یک شیء Bitmap جدید را به عنوان بوم خالی و به اندازه کادر برش ایجاد میکنیم تا تصویر برش خورده در آن قرار بگیرد و سپس توسط متد DrawImage میخواهیم که تصویر مبدا را با مختصات شیء Rectangle از نقطه 0 و 0 بوم آغاز کرده و تا انتهای آن شروع به ترسیم کند. سپس آن را ذخیره و مجددا در قالب همان آرایهای از بایتها بر میگردانیم.
تنها یک نکته را به خاطر داشته باشید که مقادیر مختصاتی که پلاگین جی کوئری ارسال میکند در قالب اعداد اعشاری هستند و برای ارسال و دریافت آنها در سرور این نکته را به خاطر داشته باشید.
مطالب
OpenCVSharp #16
در قسمت قبل با نحوهی استفاده از یک trained data از پیش آماده شدهی تشخیص چهره، آشنا شدیم. در این قسمت قصد داریم با نحوهی تولید این فایلهای XML آشنا شویم و یک تشخیص دهندهی سفارشی را طراحی کنیم.
طراحی classifier سفارشی تشخیص خودروها
برای طراحی یک تشخیص دهندهی سفارشی مبتنی بر الگوریتمهای Machine learning، نیاز به تعداد زیادی تصویر داریم. در اینجا از بانک تصاویر خودروهای «UIUC Image Database for Car Detection» استفاده خواهیم کرد. در این بسته، یک سری تصویر positive و negative را میتوان ملاحظه کرد. تصاویر مثبت، تصاویر انواع و اقسام خودروها هستند (550 عدد) و تصاویر منفی، تصاویر غیر خودرویی (500 عدد)؛ یا به عبارتی، هر تصویری، منهای تصاویر خودرو میتواند تصویر منفی باشد.
ایجاد فایل برداری از تصاویر خودروها
در ادامه یک فایل متنی را به نام carImages.txt ایجاد میکنیم. هر سطر این فایل چنین فرمتی را خواهد داشت:
ابتدا مسیر تصویر مشخص میشود. سپس عدد 1 به این معنا است که در این تصویر فقط یک عدد خودرو وجود دارد. 4 عدد بعدی، ابعاد مستطیلی تصویر هستند.
در ادامه فایل متنی دیگری را به نام negativeImages.txt جهت درج اطلاعات تصاویر منفی، ایجاد میکنیم. اینبار هر سطر این فایل تنها حاوی مسیر تصویر مدنظر است:
این دو فایل را میتوان با استفاده از دو متد ذیل، به سرعت تولید کرد:
برای کامپایل اطلاعات فایلهای تولید شده، نیاز به فایل opencv_createsamples.exe است. این فایل را در پوشهی opencv\build\x86\vc12\bin بستهی اصلی OpenCV میتوانید پیدا کنید.
پارامترهای این دستور شامل سوئیچ info است؛ به معنای مشخص سازی فایل اطلاعات تصاویر مثبت. سوئیچ num تعداد تصاویر آنرا تعیین میکند و سوئیچهای w و h، طول و عرض تصاویر هستند. سوئیچ vec نیز جهت تولید یک فایل vector از این اطلاعات بکار میرود.
پس از اجرای این دستور، فایل cars.vec تولید خواهد شد؛ با این خروجی:
اگر علاقمند هستید که محتویات فایل باینری cars.vec را مشاهده کنید، دستور ذیل را صادر نمائید:
در این پنجرهی باز شده، تصاویر بعدی و قبلی را میتوان با دکمههای arrow صفحه کلید، مشاهده کرد.
تبدیل فایل برداری تصاویر خودروها به trained data
تا اینجا موفق شدیم بیش از 500 تصویر خودرو را تبدیل به یک فایل برداری سازگار با OpenCV کنیم. اکنون نیاز است، این اطلاعات پردازش شده و trained data مخصوص الگوریتمهای machine learning تولید شود. اینکار را توسط برنامهی opencv_traincascade.exe انجام خواهیم داد. این فایل نیز در پوشهی opencv\build\x86\vc12\bin بستهی اصلی OpenCV موجود است.
دستور ذیل در پوشهی data، بر اساس اطلاعات برداری cars.vec و همچنین تصاویر منفی مشخص شدهی در فایل negativeImages.txt، با تعداد هر کدام 500 عدد (این عدد را توصیه شدهاست که اندکی کمتر از تعداد max موجود مشخص کنیم) و تعداد مراحل 2 (هر چقدر این تعداد مراحل بیشتر باشد، فایل نهایی تولید شده دقت بالاتری خواهد داشت؛ اما تولید آن به زمان بیشتری نیاز دارد) اجرا میشود. در اینجا featureType به LBP یا Local binary Pattern، تنظیم شدهاست. این الگوریتم از Haar cascade سریعتر است.
خروجی اجرای این دستور را میتوانید در پوشهی data با نام cascade.xml، پیدا کنید. پس از آن، روش استفادهی از این فایل، با مطلب تشخیص چهره تفاوتی ندارد.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
طراحی classifier سفارشی تشخیص خودروها
برای طراحی یک تشخیص دهندهی سفارشی مبتنی بر الگوریتمهای Machine learning، نیاز به تعداد زیادی تصویر داریم. در اینجا از بانک تصاویر خودروهای «UIUC Image Database for Car Detection» استفاده خواهیم کرد. در این بسته، یک سری تصویر positive و negative را میتوان ملاحظه کرد. تصاویر مثبت، تصاویر انواع و اقسام خودروها هستند (550 عدد) و تصاویر منفی، تصاویر غیر خودرویی (500 عدد)؛ یا به عبارتی، هر تصویری، منهای تصاویر خودرو میتواند تصویر منفی باشد.
ایجاد فایل برداری از تصاویر خودروها
در ادامه یک فایل متنی را به نام carImages.txt ایجاد میکنیم. هر سطر این فایل چنین فرمتی را خواهد داشت:
pos/pos-177.pgm 1 0 0 100 40
در ادامه فایل متنی دیگری را به نام negativeImages.txt جهت درج اطلاعات تصاویر منفی، ایجاد میکنیم. اینبار هر سطر این فایل تنها حاوی مسیر تصویر مدنظر است:
neg/neg-274.pgm
private static void createCarImagesFile() { var sb = new StringBuilder(); foreach (var file in new DirectoryInfo(@"..\..\CarData\CarData\TrainImages").GetFiles("*pos-*.pgm")) { sb.AppendFormat("{0} {1} {2} {3} {4} {5}{6}", file.FullName, 1, 0, 0, 100, 40, Environment.NewLine); } File.WriteAllText(@"..\..\CarsInfo\carImages.txt", sb.ToString()); } private static void createNegativeImagesFile() { var sb = new StringBuilder(); foreach (var file in new DirectoryInfo(@"..\..\CarData\CarData\TrainImages").GetFiles("*neg-*.pgm")) { sb.AppendFormat("{0}{1}", file.FullName,Environment.NewLine); } File.WriteAllText(@"..\..\CarsInfo\negativeImages.txt", sb.ToString()); }
opencv_createsamples.exe -info carImages.txt -num 550 -w 48 -h 24 -vec cars.vec
پس از اجرای این دستور، فایل cars.vec تولید خواهد شد؛ با این خروجی:
Info file name: carImages.txt Img file name: (NULL) Vec file name: cars.vec BG file name: (NULL) Num: 550 BG color: 0 BG threshold: 80 Invert: FALSE Max intensity deviation: 40 Max x angle: 1.1 Max y angle: 1.1 Max z angle: 0.5 Show samples: FALSE Original image will be scaled to: Width: $backgroundWidth / 48 Height: $backgroundHeight / 24 Create training samples from images collection... Done. Created 550 samples
"c:\opencv\build\x86\vc12\bin\opencv_createsamples.exe" -vec cars.vec -w 48 -h 24
در این پنجرهی باز شده، تصاویر بعدی و قبلی را میتوان با دکمههای arrow صفحه کلید، مشاهده کرد.
تبدیل فایل برداری تصاویر خودروها به trained data
تا اینجا موفق شدیم بیش از 500 تصویر خودرو را تبدیل به یک فایل برداری سازگار با OpenCV کنیم. اکنون نیاز است، این اطلاعات پردازش شده و trained data مخصوص الگوریتمهای machine learning تولید شود. اینکار را توسط برنامهی opencv_traincascade.exe انجام خواهیم داد. این فایل نیز در پوشهی opencv\build\x86\vc12\bin بستهی اصلی OpenCV موجود است.
دستور ذیل در پوشهی data، بر اساس اطلاعات برداری cars.vec و همچنین تصاویر منفی مشخص شدهی در فایل negativeImages.txt، با تعداد هر کدام 500 عدد (این عدد را توصیه شدهاست که اندکی کمتر از تعداد max موجود مشخص کنیم) و تعداد مراحل 2 (هر چقدر این تعداد مراحل بیشتر باشد، فایل نهایی تولید شده دقت بالاتری خواهد داشت؛ اما تولید آن به زمان بیشتری نیاز دارد) اجرا میشود. در اینجا featureType به LBP یا Local binary Pattern، تنظیم شدهاست. این الگوریتم از Haar cascade سریعتر است.
"E:\opencv\bin\opencv_traincascade.exe" -data data -vec cars.vec -bg negativeImages.txt -numPos 500 -numNeg 500 -numStages 2 -w 48 -h 24 -featureType LBP
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
مطالب
MongoDB #14
عمل تکثیر در MongoDB
عمل تکثیر (Replication) به فرآیند همزمان سازی داده در میان چند سرور گفته میشود. تکثیر، افزونگی را فراهم میآورد و دسترسی پذیری دادهها را توسط کپی داده در چندین سرور مختلف افزایش میدهد. این کار، یک پایگاه داده را در مقابل از دسترس خارج شدن یک سرور مفرد، محافظت میکند. همچنین امکان بازیابی از خرابی سخت افزار و وقفههای سرویس را به کاربر میدهد. توسط کپی برداری از اطلاعات، میتوانید یکی از آنها را برای بازیابی، گزارشگیری و پشتیبان گیری اختصاص دهید.
چرا تکثیر؟
- برای ایمن نگه داری اطلاعات
- دسترسی پذیری بالای اطلاعات (شبانه روزی)
- بازیابی اطلاعات
- نیازی به از کار افتادن هنگام انجام عملیات نگهداری ندارد
- مقایس پذیری خواندن دادهها (کپی برداریهای اضافه برای عمل خواندن)
- کپی اطلاعات برای نرم افزارها شفاف و قابل دستیابی است.
تکثیر در MongoDB چگونه کار میکند
MongoDB عمل تکثیر را با استفاده از مجموعه کپی یا المثنی (Replica set) انجام میدهد. مجموعه کپی یک گروه از نمونههای mongodb هستند که مجموعه داده یا دیتاست مشابهی را میزبانی (Host) میکنند. در یک کپی داده، یک گره، گره اصلی است که تمام عملیات نوشتن را دریافت میکند. بقیهی نمونههای ثانویه، عملیات را از گره اصلی، دریافت و اعمال میکنند؛ بنابراین آنها هم دیتاست مشابهی دارند. مجموعهی کپی تنها میتواند یک گرهی اصلی داشته باشد.
- یک مجموعهی کپی، یک گروه از دویا چند گره است. (عموما حداقل 3 گره نیاز است.)
- در یک مجموعهی کپی، یک گره، گره اصلی است و بقیه گرهها گرههای ثانویه هستند.
- همهی دادهها از گرهی اصلی به گرههای ثانویه تکثیر میشوند.
- هنگام انجام عملیات نگه داری یا ازدسترس خارج شدن سرور، گزینش برای گره اصلی و انتخاب گره اصلی جدید آغاز میشود.
- گره از کار افتاده، بعد از بازیابی دوباره، به مجموعه کپی ملحق میشود و بعنوان یک گره ثانویه کار میکند.
در زیر یک نوع دیاگرام از تکثیر در MongoDB نشان داده شده است که در آن برنامهی سمت کلاینت همیشه با گرهی اصلی در ارتباط است و گرهی اصلی، دادهها را گرههای ثانویه تکثیر میکند.
ویژگیهای مجموعهی کپی
- یک کلاستر از N عدد گره
- هر گرهایی میتواند گره اصلی باشد
- همهی عملیات نوشتن بر روی گره اصلی انجام میشود
- عمل ازدسترس خارج شدن سرور و جایگزین شدن یک گره بصورت اتوماتیک
- بازیابی بصورت اتوماتیک
- همراهی و توافق در گزینش گره اصلی
ساختن یک مجموعه کپی
در اینجا میخواهیم یک نمونه از mongodb را به یک مجموعهی کپی تبدیل کنیم. برای این کار مراحل زیر را انجام دهید:
- همهی نمونههای در حال اجرای mongod را در سمت سرور، متوقف کنید.
- اکنون mongod سمت سرور را با سوئیچ –replSet راه اندازی کنید.
گرامر پایه --replSet به شکل زیر است:
mongod --port "PORT" --dbpath "YOUR_DB_DATA_PATH" --replSet "REPLICA_SET_INSTANCE_NAME"
mongod --port 27017 --dbpath "D:\set up\mongodb\data" --replSet rs0
افزودن اعضا به مجموعهی کپی
برای افرودن اعضا به مجموعهی کپی، چند نمونه mongodb را در چندین کامپیوتر راه اندازی کنید. اکنون برنامهی سمت کلاینت را اجرا و دستور ()rs.add را اجرا کنید.
گرامر
گرامر پایه دستور ()rs.add به شکل زیر است:
>rs.add(HOST_NAME:PORT)
فرض کنید نام نمونهی mongodb شما mongod1.net و بر روی پورت 27017 در حال اجراست. برای افزودن این نمونه به مجموعه کپی، دستور () rs.add را در سمت کلاینت اجرا کنید.
>rs.add("mongod1.net:27017") >