مطالب
امکان استفاده از کتابخانه‌های native در Blazor WASM 6x
کتابخانه‌‌های بسیاری هستند که به زبان‌های C ، C++ ، Rust و امثال آن تهیه شده‌اند. دات نت 6، قابلیت جدید استفاده‌ی از این نوع کتابخانه‌ها را بدون نیاز به تبدیل کدهای آن‌ها به #C، به برنامه‌های سمت کلاینت Blazor Web Assembly اضافه کرده که در این مطلب، نمونه‌ای از آن‌را با استفاده از بانک اطلاعاتی SQLite در برنامه‌های Blazor WASM 6x، بررسی خواهیم کرد. یعنی یک برنامه‌ی SPA سمت کلاینت که بدون نیاز به سرور و Web API، تنها با استفاده از EF-Core و بانک اطلاعاتی بومی SQLite می‌تواند اطلاعات مورد نیاز خود را ثبت و یا بازیابی کند (همه چیز داخل مرورگر رخ می‌دهد).


ایجاد یک پروژه‌ی Blazor WASM جدید

یک پوشه‌ی جدید دلخواه را به نام BlazorWasmSQLite ایجاد کرده و با اجرای دستور dotnet new blazorwasm، یک پروژه‌ی Blazor Web Assembly خالی جدید را در آن آغاز می‌کنیم. همانطور که از دستور نیز مشخص است، این پروژه از نوع hosted که به همراه Web API هم هست، نمی‌باشد.


افزودن Context و مدل EF-Core به برنامه

مدل برنامه به صورت زیر در پوشه‌ی Models آن قرار می‌گیرد:
namespace BlazorWasmSQLite.Models;

public class Car
{
  public int Id { get; set; }

  public string Brand { get; set; }

  public  int Price { get; set; }
}
و Context ای که آن‌را در معرض دید قرار می‌دهد، به صورت زیر تعریف خواهد شد:
using Microsoft.EntityFrameworkCore;
using BlazorWasmSQLite.Models;

namespace BlazorWasmSQLite.Data;

public class ClientSideDbContext : DbContext
{
  public DbSet<Car> Cars { get; set; } = default!;

  public ClientSideDbContext(DbContextOptions<ClientSideDbContext> options) :
    base(options)
  {
  }
}
همچنین چون می‌خواهیم از بانک اطلاعاتی SQLite استفاده کنیم، وابستگی زیر را به فایل csproj برنامه اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <!-- EF Core and Sqlite -->
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
  </ItemGroup>
</Project>
سپس این Context را به نحو زیر به فایل Program.cs معرفی می‌کنیم:
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorWasmSQLite;
using Microsoft.EntityFrameworkCore;
using BlazorWasmSQLite.Data;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

// Sets up EF Core with Sqlite
builder.Services.AddDbContextFactory<ClientSideDbContext>(options =>
      options
        .UseSqlite($"Filename=DemoData.db")
        .EnableSensitiveDataLogging());

await builder.Build().RunAsync();
در مورد علت استفاده‌ی از AddDbContextFactory و نکات مرتبط با آن، به مطلب «نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server» مراجعه نمائید.


ثبت تعدادی رکورد در بانک اطلاعاتی

در ادامه سعی می‌کنیم در فایل Index.razor، تعدادی رکورد را به بانک اطلاعاتی اضافه کنیم:
@page "/"

@using Microsoft.Data.Sqlite
@using Microsoft.EntityFrameworkCore
@using BlazorWasmSQLite.Data
@using BlazorWasmSQLite.Models


<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

@code {
  [Inject]
  private IDbContextFactory<ClientSideDbContext> _dbContextFactory { get; set; } = default!;

  protected override async Task OnInitializedAsync()
  {
    await using var db = await _dbContextFactory.CreateDbContextAsync();
    await db.Database.EnsureCreatedAsync();

    // create seed data
    if (!db.Cars.Any())
    {
      var cars = new[]
      {
        new Car { Brand = "Audi", Price = 21000 },
        new Car { Brand = "Volvo", Price = 11000 },
        new Car { Brand = "Range Rover", Price = 135000 },
        new Car { Brand = "Ford", Price = 8995 }
      };

      await db.Cars.AddRangeAsync(cars);
      await db.SaveChangesAsync();
    }

    await base.OnInitializedAsync();
  }
}
در این مثال سعی شده‌است ساده‌ترین حالت ممکن کار با EF-Core در پیش گرفته شود؛ چون هدف اصلی آن، دسترسی به SQLite است.


اولین سعی در اجرای برنامه

در ادامه سعی می‌کنیم تا برنامه را اجرا کنیم. با خطای زیر متوقف خواهیم شد:
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component:
The type initializer for 'Microsoft.Data.Sqlite.SqliteConnection' threw an exception. System.TypeInitializationException:
The type initializer for 'Microsoft.Data.Sqlite.SqliteConnection' threw an exception. ---> System.Reflection.TargetInvocationException:
Exception has been thrown by the target of an invocation. ---> System.DllNotFoundException: e_sqlite3 at
SQLitePCL.SQLite3Provider_e_sqlite3.SQLitePCL.ISQLite3Provider.sqlite3_libversion_number()
عنوان می‌کند که فایل‌های بانک اطلاعاتی SQLite به همراه EF-Core را نمی‌تواند پیدا کند. یا به عبارتی هر DLL بومی را نمی‌توان داخل مرورگر اجرا کرد.


رفع مشکل کار با SQLite با کامپایل ویژه‌ی آن

برای دسترسی به کدهای native در Blazor WASM و مرورگر، باید آن‌ها را توسط کامپایلر emcc به صورت زیر کامپایل کرد:
$ git clone https://github.com/cloudmeter/sqlite
$ cd sqlite
$ emcc sqlite3.c -shared -o e_sqlite3.o
در اینجا هر نوع فایل portable native code با فرمت‌های o. یا object files، .a و یا archive files و یا .bc یا bitcode و یا .wasm یا Standalone WebAssembly modules توسط Blazor wasm قابل استفاده هستند که در مثال فوق نمونه‌ی object files آن‌ها توسط کامپایلر تولید می‌شود.
مرحله‌ی بعد، معرفی این object file تولید شده به برنامه است. برای اینکار ابتدا باید dotnet workload install wasm-tools را نصب کرد (مهم). سپس به فایل csproj برنامه مراجعه کرده و فایل e_sqlite3.o را به آن معرفی می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <!-- EF Core and Sqlite -->
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
    <NativeFileReference Include="Data\e_sqlite3.o" />
  </ItemGroup>
</Project>
در اینجا فرض شده‌است که فایل o. حاصل، در پوشه‌ی data قرار دارد. این نوع فایل‌ها توسط NativeFileReferenceها به برنامه معرفی می‌شوند.


سعی در اجرای مجدد برنامه

پس از نصب wasm-tools و ذکر NativeFileReference فوق، اکنون اگر برنامه را اجرا کنیم، برنامه بدون مشکل اجرا خواهد شد:



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmSQLite.zip
اشتراک‌ها
دوره آموزشی چهار ساعته Blazor Hybrid

Learn Blazor Hybrid - Full Course for Beginners | Build cross-platform apps in C#

Let's start our journey together to build beautiful native cross-platform apps for iOS, Android, macOS, and Windows with Blazor Hybrid, .NET MAUI, C#, and Visual Studio! In this full workshop, I will walk you through everything you need to know about .NET MAUI and building your very first app. You will learn the basics including how to build user interfaces with Razor, how to show data from the internet, how to navigate between pages and combine .NET MAUI pages with Razor pages, access platform features like geolocation, and theme your app for light theme and dark theme. This course has everything you need to learn the basics and set you up for success when building apps with Blazor Hybrid!

Chapters:

00:00:00 - Intro to the Blazor Hybrid Workshop

00:04:28 - What is Blazor Hybrid & How to Install

00:06:51 - First Blazor Hybrid App & Architecture

00:21:40 - Get Code to Build Your First Blazor Hybrid App

00:26:38 - Blazor Hybrid Project Walkthrough

00:39:22 - Start to Build First Blazor Hybrid App

01:03:10 - Event Handling, Data Binding and Parameters (Slides)

01:09:00 - Add Monkey Data & Fluent UI Blazor Components

01:32:08 - Navigation, NavigationManager, .NET MAUI Pages (Slides)

01:39:19 - Navigation with NavigationManager

01:52:39 - Navigation with NavLinks

01:57:21 - Add .NET MAUI Pages & Components

02:21:11 - Access Platform Functionality (Slides)

02:27:57 - Check Network Connectivity

02:38:04 - Get User Location with Geolocation

02:49:09 - Integration with Other Apps

02:57:42 - App Theming, Light Theme, Dark Theme (Slides)

03:05:36 - JavaScript Interoperability with IJSRuntime

03:20:48 - Theming FluentUI Blazor Components

03:26:05 - Style Status Bar with .NET MAUI Community Toolkit

03:39:00 - .NET MAUI Light & Dark Theme with AppThemeBinding

03:42:58 - Sharing State & Creating Reusable Components (Slides)

03:47:27 - Implement Shared State Blazor Hybrid & .NET MAUI

04:02:47 - Create Reusable Razor Components

04:08:31 - CONGRATULATIONS!

دوره آموزشی چهار ساعته Blazor Hybrid
اشتراک‌ها
چه تفاوتی بین Update و Upgrade است؟

Update and upgrade are two different ways to make a change to an app or operating system. But the prime difference lies in a number of modifications made and the importance of those modifications. A software update includes bug fixes, and other small improvements, while a software upgrade changes the version of a software

چه تفاوتی بین Update و Upgrade است؟
اشتراک‌ها
دوره 8 ساعته آموزش Blazor

⭐️ Course Contents ⭐️
⌨️ (0:00:00​) Introduction
⌨️ (0:00:34​) Blazor Architecture Overview
⌨️ (0:11:59​) Blazor Architecture In Depth
⌨️ (0:24:00​) Blazor Hosting Models
⌨️ (0:32:23​) Project Structure
⌨️ (0:48:31​) Data Binding
⌨️ (1:12:21​) Components Basics
⌨️ (1:33:33​) Communication Between Components Overview
⌨️ (1:37:59​) Component Parameters
⌨️ (1:45:03​) Route Parameters
⌨️ (1:57:05​) Cascading Parameters
⌨️ (2:14:58​) EventCallBack
⌨️ (2:27:16​) Referencing Child Components
⌨️ (2:40:06​) Templated Components - RenderFragment
⌨️ (2:51:31​) Templated Components - Generic Typed Item List
⌨️ (3:07:01​) Templated Components - Generic Typed RenderFragment
⌨️ (3:27:27​) Templated Components - Generic Typed RenderFragment as a Form
⌨️ (3:37:30​) When is Rendering Triggered
⌨️ (3:59:45​) Lifecycle Events Sequence
⌨️ (4:11:39​) Avoiding Data Initialization Pitfall
⌨️ (4:16:15​) Forms and Validations
⌨️ (4:22:00​) Dependency Injection
⌨️ (4:49:34​) State Management with Flux in C#
⌨️ (5:34:37​) Authentication
⌨️ (6:04:18​) Creating a reusable DataGrid Component - Columns Configuration
⌨️ (6:36:27​) Creating a reusable DataGrid Component - Paging
⌨️ (7:16:49​) Creating a reusable DataGrid Component - Sorting
⌨️ (7:42:51​) Thank you and My Contact Info
 

دوره 8 ساعته آموزش Blazor
مطالب
Blazor 5x - قسمت 18 - کار با فرم‌ها - بخش 6 - حذف اطلاعات
در این قسمت می‌خواهیم اطلاعات اتاق‌های ثبت شده را به همراه تصاویر مرتبط با آن‌ها، حذف کنیم و همچنین به یک خطای مهم در حین کار با EF-Core برسیم و متوجه شویم که روش کار با DbContext در برنامه‌های مبتنی بر Blazor Server .... با روش کار متداول با آن در برنامه‌های Web API، یکی نیست!


مشکل حذف تصاویر آپلود شده

در قسمت قبل، این امکان را مهیا کردیم که کاربران بتوانند پیش از ثبت اطلاعات یک اتاق، تصاویر آن‌را به سرور آپلود کنند. یعنی تصاویری که در ابتدای کار آپلود می‌شوند، هنوز در بانک اطلاعاتی ثبت نشده‌اند و هیچ رکوردی از آن‌ها موجود نیست. در این حالت اگر کاربری تصاویری را آپلود کرده و سپس بر روی دکمه‌ی back کلیک کند، با تعدادی تصویر آپلود شده‌ی غیرمنتسب به اتاق‌های موجود، مواجه خواهیم شد. همچنین اگر شخصی به قسمت ویرایش تصاویر مراجعه کند و با کلیک بر روی دکمه‌ی حذف یک تصویر، آن‌را حذف کند، این حذف باید در بانک اطلاعاتی هم منعکس شود؛ در غیر اینصورت باز هم کاربر می‌تواند تصویری را حذف کند، اما در آخر بر روی دکمه‌ی به روز رسانی اطلاعات رکورد کلیک نکند. در این حالت در دفعات بعدی مراجعه‌ی به اطلاعات یک چنین اتاقی، با نقص اطلاعات تصاویری مواجه می‌شویم که در لیست تصاویر منتسب به یک اتاق وجود دارند، اما اصل فایل تصویری متناظر با آن‌ها از سرور حذف شده‌است.


حذف اطلاعات تصاویر، در حالت ثبت اطلاعات


زمانیکه قرار است اطلاعات اتاقی برای اولین بار ثبت شود، حذف تصاویر آپلود شده‌ی مرتبط با آن ساده‌است؛ چون هنوز اصل رکورد اتاق ثبت نشده‌است و این تصاویر در این لحظه، به رکوردی تعلق ندارند. بنابراین ابتدا متد رویدادگردان DeletePhoto را به دکمه‌ی حذف اطلاعات هر تصویر نمایش داده شده، انتساب می‌دهیم:
@if (HotelRoomModel.HotelRoomImages.Count > 0)
{
    var serial = 1;
    foreach (var roomImage in HotelRoomModel.HotelRoomImages)
    {
        <div class="col-md-2 mt-3">
            <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; ">
                <span class="room-image-title">@serial</span>
            </div>
            <button type="button"
                    @onclick="()=>DeletePhoto(roomImage)"
                    class="btn btn-outline-danger btn-block mt-4">Delete</button>
        </div>
        serial++;
    }
}
و سپس آن‌را به صورت زیر تکمیل می‌کنیم:
@code
{
    private const string UploadFolder = "Uploads";

    private void DeletePhoto(HotelRoomImageDTO imageDto)
    {
        var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase);
        if (HotelRoomModel.Id == 0 && Title == "Create")
        {
            FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder);
            HotelRoomModel.HotelRoomImages.Remove(imageDto);
        }
    }
}
- با هر بار کلیک بر روی دکمه‌ی Delete، شیء HotelRoomImageDTO متناظری به متد DeletePhoto ارسال می‌شود.
- در این شیء، مقدار خاصیت RoomImageUrl، همواره با نام پوشه‌ای که فایل‌های تصویری در آن آپلود شده‌اند، شروع می‌شود. به همین جهت نام پوشه را از آن حذف کرده و بر این اساس، متد FileUploadService.DeleteFile را فراخوانی می‌کنیم تا تصویر جاری را از سرور حذف کند.
- سپس با فراخوانی متد Remove بر روی لیست تصاویر موجود، سبب به روز رسانی UI نیز خواهیم شد و به این ترتیب، تصویری که فایل آن از سرور حذف شده، از UI نیز حذف خواهد شد.


حذف تصاویر، در زمان ویرایش اطلاعات یک اتاق تعریف شده

همانطور که در ابتدای بحث نیز عنوان شد، نمی‌خواهیم در حالت ویرایش یک رکورد، با کلیک بر روی حذف یک تصویر، بلافاصله آن‌را از سرور نیز حذف کنیم. چون ممکن است کاربری تصویری را حذف کند، اما بجای ذخیره سازی اطلاعات رکورد، بر روی دکمه‌ی back کلیک کند. بنابراین در اینجا حذف تصاویر را صرفا به حذف آن‌ها از UI محدود می‌کنیم و حذف نهایی را به زمان کلیک بر روی دکمه‌ی ذخیره سازی اطلاعات در حال ویرایش، موکول خواهیم کرد.
به همین جهت در ابتدا با کلیک بر روی دکمه‌ی حذف، ابتدا با حذف آن تصویر از HotelRoomImages، سبب به روز رسانی UI خواهیم شد، اما این تصویر را واقعا حذف نمی‌کنیم. در اینجا فقط نام آن‌را در یک لیست، برای حذف نهایی، ذخیره سازی خواهیم کرد:
@code
{
    private List<string> DeletedImageFileNames = new List<string>();

    private void DeletePhoto(HotelRoomImageDTO imageDto)
    {
        var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase);
        if (HotelRoomModel.Id == 0 && Title == "Create")
        {
            // ...              
        }
        else
        {
            // Edit Mode
            DeletedImageFileNames.Add(imageFileName);
            HotelRoomModel.HotelRoomImages.Remove(imageDto); // Update UI
        }
    }
به این ترتیب اگر کاربر بر روی دکمه‌ی back کلیک کند، اتفاق خاصی رخ نمی‌دهد؛ نه رکوردی از بانک اطلاعاتی و نه فایل تصویری از سرور حذف می‌شود.

سپس در جائیکه کار مدیریت ثبت اطلاعات صورت می‌گیرد، پس از به روز رسانی رکورد متناظر با یک اتاق، بر اساس لیست DeletedImageFileNames، فایل‌های علامتگذاری شده‌ی برای حذف را نیز واقعا از سرور حذف می‌کنیم:
    private async Task HandleHotelRoomUpsert()
    {
        // ...
 
        if (HotelRoomModel.Id != 0 && Title == "Update")
        {
            // Update Mode
            var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel);

            foreach(var imageFileName in DeletedImageFileNames)
            {
                FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder);
            }

            // await AddHotelRoomImageAsync(updatedRoomDto);
            await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully.");
        }
        else
        {
           // ... 
        }
    }
}
در اینجا باز هم نیازی نیست تا یک حلقه را تشکیل دهیم و اطلاعات را مستقیما از جدول تصاویر حذف کنیم. HotelRoomModel ارسال شده‌ی به متد UpdateHotelRoomAsync، چون به همراه لیست جدید HotelRoomImages است (که توسط فراخوانی HotelRoomModel.HotelRoomImages.Remove به روز شده‌است)، در حین Update، تصاویری که در این لیست وجود نداشته باشند، به صورت خودکار توسط EF-Core از سر دیگر رابطه حذف می‌شوند.


نمایش «لطفا منتظر بمانید» در حین آپلود تصاویر

در ادامه می‌خواهیم تا پایان نمایش آپلود تصاویر، پیام «لطفا منتظر بمانید» را به همراه یک spinner نمایش دهیم. بنابراین در ابتدا کلاس‌های جدید زیر را به فایل wwwroot\css\site.css اضافه می‌کنیم:
.spinner {
  border: 16px solid silver !important;
  border-top: 16px solid #337ab7 !important;
  border-radius: 50% !important;
  width: 80px !important;
  height: 80px !important;
  animation: spin 700ms linear infinite !important;
  top: 50% !important;
  left: 50% !important;
  transform: translate(-50%, -50%);
  position: absolute !important;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}
سپس برای مدیریت نمایش spinner فوق، در ابتدای کار آپلود، فیلدIsImageUploadProcessStarted را به true تنظیم کرده و در پایان کار، آن‌را false می‌کنیم. به همین جهت نیاز به یک try/finally خواهد بود:
@code
{
    private bool IsImageUploadProcessStarted;

    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {
        try
        {
            IsImageUploadProcessStarted = true;
            // ...
        }
        finally
        {
            IsImageUploadProcessStarted = false;
        }
    }
}
پس از آن فقط کافی است بر اساس مقدار جاری این فیلد، ذیل فیلد InputFile، پیامی را نمایش دهیم:
<InputFile OnChange="HandleImageUpload" multiple></InputFile>
<div class="row">
@if (IsImageUploadProcessStarted)
{
    <div class="col-md-12">
        <span><i class="spinner"></i> Please wait.. Images are uploading...</span>
    </div>
}

دریافت تائیدیه‌ی حذف، پس از کلیک بر روی دکمه‌های حذف تصاویر


در قسمت 12 این سری، کامپوننت Confirmation.razor را توسعه دادیم. در اینجا می‌خواهیم با کلیک بر روی دکمه‌ها‌ی حذف تصاویر، ابتدا توسط این کامپوننت، تائیدیه‌ای دریافت شود و در صورت تائید، آن تصویر انتخابی را حذف کنیم.
به همین جهت در ابتدا فایل Confirmation.razor را به پوشه‌ی جدید Pages\Components کپی می‌کنیم. سپس فضای نام آن‌را به فایل BlazorServer\BlazorServer.App\_Imports.razor اضافه می‌کنیم تا در تمام کامپوننت‌های برنامه قابل استفاده شود:
@using BlazorServer.App.Pages.Components
سپس در ابتدا کامپوننت Confirmation را به صورت زیر اضافه می‌کنیم:
<Confirmation @ref="Confirmation1"
    OnCancel="OnCancelDeleteImageClicked"
    OnConfirm="@(()=>OnConfirmDeleteImageClicked(ImageToBeDeleted))">
    <div>
        Do you want to delete @ImageToBeDeleted?.RoomImageUrl image?
    </div>
</Confirmation>
- ref تعریف شده سبب می‌شود تا بتوان متدهای عمومی تعریف شده‌ی در این کامپوننت، مانند Show و Hide را فراخوانی کرد.
- سپس روال‌های رویدادگردان OnCancel و OnConfirm به متدهایی در کامپوننت جاری متصل شده‌اند.
- در آخر پیامی تعریف شده‌است.

برای اینکه کامپوننت فوق عمل کند، نیاز است تغییرات زیر را به قسمت کدها اعمال کنیم:
    private Confirmation Confirmation1;
    private HotelRoomImageDTO ImageToBeDeleted;

    private void OnCancelDeleteImageClicked()
    {
        // Confirmation1.Hide();
    }

    private void DeletePhoto(HotelRoomImageDTO imageDto)
    {
        ImageToBeDeleted = imageDto;
        Confirmation1.Show();
    }

    private void OnConfirmDeleteImageClicked(HotelRoomImageDTO imageDto)
    {
- توسط وهله‌ی Confirmation1، می‌توان متد Show را زمانیکه بر روی دکمه‌ی Delete هر تصویر کلیک می‌شود، فراخوانی کنیم. قبل از آن مشخصات شیء تصویر درخواستی را در فیلد ImageToBeDeleted ذخیره می‌کنیم تا پس از تائید کاربر، دقیقا بر اساس اطلاعات آن بتوانیم متد OnConfirmDeleteImageClicked را پردازش کنیم.
- در اینجا محتوای متد DeletePhoto اصلی را (متدی را که تا پیش از این مرحله تکمیل کردیم) به متد جدید OnConfirmDeleteImageClicked منتقل کرده‌ایم. یعنی در ابتدا فقط یک modal نمایش داده می‌شود. پس از اینکه کاربر عملیات حذف را تائید کرد، رویداد OnConfirm، سبب فراخوانی متد OnConfirmDeleteImageClicked خواهد شد (که همان DeletePhoto قبل از این تغییرات است).


حذف کامل یک اتاق به همراه تمام تصاویر منتسب به آن

مرحله‌ی آخر این قسمت، اضافه کردن دکمه‌ی حذف، به ردیف‌های کامپوننت نمایش لیست اتاق‌ها است که این مورد نیز باید به همراه دریافت تائیدیه‌ی حذف و همچنین حذف تمام وابستگی‌های اتاق ثبت شده باشد:
<td>
    <NavLink href="@($"hotel-room/edit/{room.Id}")" class="btn btn-primary">Edit</NavLink>
    <button class="btn btn-danger" @onclick="()=>HandleDeleteRoom(room)">Delete</button>
</td>
در کامپوننت BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomList.razor، دکمه‌ی Delete را به نحو فوق اضافه کرده‌ایم که با کلیک بر روی آن، روال رویدادگردان HandleDeleteRoom اجرا شده و room متناظری را دریافت می‌کند.
اکنون برای مدیریت دریافت تائیدیه‌ی حذف از کاربر، کامپوننت Confirmation را اضافه کرده:
<Confirmation @ref="Confirmation1"
    OnCancel="OnCancelDeleteRoomClicked"
    OnConfirm="OnConfirmDeleteRoomClicked">
    <div>
        Do you want to delete @RoomToBeDeleted?.Name?
    </div>
</Confirmation>
و به نحو زیر تکمیل می‌کنیم:
@code
{
    private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>();
    private HotelRoomDTO RoomToBeDeleted;
    private Confirmation Confirmation1;

    private void OnCancelDeleteRoomClicked()
    {
        // Confirmation1.Hide();
    }

    private void HandleDeleteRoom(HotelRoomDTO roomDto)
    {
        RoomToBeDeleted = roomDto;
        Confirmation1.Show();
    }

    private async Task OnConfirmDeleteRoomClicked()
    {
        if(RoomToBeDeleted is null)
        {
            return;
        }

        await HotelRoomService.DeleteHotelRoomAsync(RoomToBeDeleted.Id);
        HotelRooms.Remove(RoomToBeDeleted); // Update UI
    }
با کلیک بر روی دکمه‌ی حذف، متد HandleDeleteRoom اجرا شده و فیلد RoomToBeDeleted را مقدار دهی می‌کند. از این فیلد پس از دریافت تائید، در متد OnConfirmDeleteRoomClicked برای حذف اتاق انتخابی استفاده شده‌است.

مشکل! این روش استفاده‌ی از DbContext کار نمی‌کند!

اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر می‌رسیم:
An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'.
System.InvalidOperationException: A second operation was started on this context before a previous operation completed.
This is usually caused by different threads concurrently using the same instance of DbContext.
For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
عنوان می‌کند که متد OnConfirmDeleteRoomClicked، بر روی ترد دیگری نسبت به ترد اولیه‌ای که DbContext بر روی آن ایجاد شده، در حال اجرا است و چون DbContext برای یک چنین سناریوهایی، thread-safe نیست، اجازه‌ی استفاده‌ی از آن‌را نمی‌دهد. در مورد روش حل این مشکل ویژه، در قسمت بعد بحث خواهیم کرد.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-18.zip
اشتراک‌ها
کتاب Getting the Most from LINQPad Succinctly

LINQPad is a powerful testing tool for all .NET developers that can help them deliver solutions in less time. In Getting the Most from LINQPad Succinctly, returning Succinctly series author José Roberto Olivas Mendoza lays out different ways to extend the functionality built into LINQPad. In this ebook, you’ll learn how to use LINQPad to query Entity Framework models in Visual Studio, how to work with the LINQPad command-line utility, how to write your own extensions and visualizers, and how to write custom data context drivers.

TABLE OF CONTENTS
  • A Quick Tour of LINQPad

  • LINQPad and Entity Framework

  • LINQPad Scripting

  • LINQPad Extensibility

  • Custom Data Context Drivers

کتاب Getting the Most from LINQPad Succinctly