نظرات مطالب
ASP.NET MVC #12
سلام
در قطعه کد
@model bool
<p>@Html.RadioButton("", false, !Model) Female</p>
<p>@Html.RadioButton("", true, Model) Male</p>
فرق model با با Model چیه؟
بعدش اینکه برای فهم قطعه کد زیر باید با چه مفاهیمی آشنا باشیم (پر کردن DropDown)
@model Enum
@Html.DropDownListFor(m => m, Enum.GetValues(Model.GetType())
         .Cast<Enum>()
         .Select(m => {
                   string enumVal = Enum.GetName(Model.GetType(), m);
                   return new SelectListItem() {
                             Selected = (Model.ToString() == enumVal),
                             Text = enumVal,
                             Value = enumVal
                  };
}))
مطالب
ایجاد سرویس چندلایه‎ی WCF با Entity Framework در قالب پروژه - 9
یک Windows Form جدید ایجاد کنید و نام آن را frmAddEditNews بگذارید. 
برابر با شکل ویژگی‌های IsDeleted، tblCategory و tblNewsId را برابر با None کنید و tblCategoryId را از نوع Combobox انتخاب کنید. سپس با فشار فلش کنار tblNews گزینه‌ی Details را انتخاب کنید.

روی tblNews کلیک کرده آن‌را بکشید و روی فرم رها کنید. آن‌گاه ظاهر فرم و چیدمان کنترل‌ها را تنظیم کنید و دو دکمه ذخیره و لغو برابر با شکل در فرم ایجاد کنید:

کد روی‌داد دو دکمه را این‌گونه بنویسید:

        private void btnCancel_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void btnSave_Click(object sender, EventArgs e)
        {
            this.DialogResult = System.Windows.Forms.DialogResult.OK;
        }

در پایین فرم روی tblNewsBindingSource کلیک کنید و از قسمت Properties ویژگی Modifiers آن‌را برابر با Public کنید. 

روی Combobox کلیک کنید، سپس ویژگی DataBinding -> Text آن‌را خالی کنید. سپس روی فلش بالای Combobox دسته خبر کلیک کنید و تنظیمات آن‌را مانند شکل زیر انجام دهید.

برای پرشدن آن کد زیر را در روی‌داد Load فرم این‌گونه بنویسید:

        private void frmAddEditNews_Load(object sender, EventArgs e)
        {
            MyNewsService.MyNewsServiceClient MyNews = new MyNewsService.MyNewsServiceClient();
            tblCategoryIdComboBox.DataSource = MyNews.GetAllCategory();
        }

به فرم اصلی بازگردید و برای روی‌داد دکمه‌ی ویرایش چنین بنویسید:

        private void btnEdit_Click(object sender, EventArgs e)
        {
            if (tblNewsDataGridView.CurrentRow == null)
            {
                MessageBox.Show("سطری برای ویرایش انتخاب کنید");
            }
            else
            {
                //tblNews news = tblNewsDataGridView.CurrentRow.DataBoundItem as tblNews;
                tblNews news  = MyNews.GetNews(Convert.ToInt32(tblNewsDataGridView.CurrentRow.Cells["tblNewsId"].Value));
                frmAddEditNews frmAdd = new frmAddEditNews();
                frmAdd.tblNewsBindingSource.DataSource = news;
                if (frmAdd.ShowDialog() == DialogResult.OK)
                {
                    MyNews.EditNews(news);
                    tblNewsBindingSource.DataSource = MyNews.GetAllNews().Select(p => new { p.tblNewsId, p.tblCategory.CatName, p.Title, p.Description, RegDate = MiladiToShamsi(p.RegDate) });
                }
            }
        }

در صورتی که متد GetAllNews را به صورت ساده به ویژگی DataSource دیتاگرید نسبت داده بودیم می‌توانستید از کد زیر برای مقداردهی به متغیر news بهره ببریم. ولی در حال حاضر این خط کد پیغام خطا می‌دهد. البته راه‌های دیگری برای حل این مشکل وجود دارد که در این درس قصد پرداختن به آن‌را ندارم.

tblNews news = tblNewsDataGridView.CurrentRow.DataBoundItem as tblNews;

کد مربوط به روی‌داد دکمه‌ی افزودن و حذف را نیز به صورت زیر بنویسید:

        private void btnAdd_Click(object sender, EventArgs e)
        {
            tblNews news = new tblNews();
            frmAddEditNews frmAdd = new frmAddEditNews();
            frmAdd.tblNewsBindingSource.DataSource = news;
            if (frmAdd.ShowDialog() == DialogResult.OK)
            {
                MyNews.AddNews(news);
                tblNewsBindingSource.DataSource = MyNews.GetAllNews().Select(p => new { p.tblNewsId, p.tblCategory.CatName, p.Title, p.Description, RegDate = MiladiToShamsi(p.RegDate) });
            }
        }

        private void btnRemove_Click(object sender, EventArgs e)
        {
            if (MessageBox.Show("آیا با حذف این سطر اطمینان دارید؟","هشدار",MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes)
            {
                MyNews.DeleteNews(Convert.ToInt32(tblNewsDataGridView.CurrentRow.Cells["tblNewsId"].Value));
                tblNewsBindingSource.DataSource = MyNews.GetAllNews().Select(p => new { p.tblNewsId, p.tblCategory.CatName, p.Title, p.Description, RegDate = MiladiToShamsi(p.RegDate) });
            }
        }

برنامه را اجرا کنید. کار ما کم و بیش به پایان رسیده است. شما یک پروژه‌ی ویندوز ساده با استفاده از WCF ای که از Entity Framework برای اتصال به پایگاه داده بهره می‌برد؛ ایجاد کردید. WCF بسیار گسترده‌تر از این است و در این‌جا تنها به بخشی از آن پرداختیم. احتمالاً در صورت استقبال خوانندگان در آینده درباره‌ی تنظیمات ریز WCF برای امنیت، سرعت، محدودیت و استفاده در محیط‌های مختلف خواهم نوشت.

شاد و پیروز باشید.

مطالب
استفاده از دیتابیس Sqlite در الکترون (قسمت اول)
یکی از مهمترین بخش‌های هر برنامه، بخش ذخیره و بازیابی دیتا است. برای ذخیره سازی از طریق وب و مرورگر، راه‌های مختلف زیادی چون webStorage ,Indexed DB ,Sqlite ,NeDB, و ... وجود دارند.

Sqlite دیتابیس مناسبی برای برنامه‌های چندسکویی است و عموما به عنوان اولین گزینه استفاده می‌شود. برای کار با این دیتابیس، ما از ماژول sql.js که یکی از ماژول‌های معروف در جاوااسکریپت است، استفاده می‌کنیم. برای نصب آن از طریق npm، به شکل زیر اقدام می‌کنیم:
npm install sql.js --save
سپس کد همیشگی زیر را برای آغاز، در فایل index.js وارد می‌کنیم:
const{app,BrowserWindow}=require("electron");

let win;
function onLoad()
{
  win=new BrowserWindow({
    width:800,
    height:600
  });
  win.loadURL(`file://${__dirname}/index.html`);
}

app.on("ready",onLoad());
  ابتدا باید بررسی کنیم که آیا دیتابیس از قبل موجود است یا خیر و اگر موجود نبود، برای اولین بار آن را بسازیم. برای بررسی وجود یک فایل نیز می‌توانیم از چند دستور مختلف استفاده کنیم. path.exists و fs.exists، دو عدد از آن‌ها می‌باشند که هر دوی آنان به صورت غیرهمزمان هستند و پارمتر اولشان نام فایل، به همراه مسیر (یا تنها مسیر) است و پارامتر دوم هم یک تابع callback است که به عنوان پارامتر، جواب را بر می‌گرداند. برای استفاده از حالت همزمان، عبارت Sync را به انتهای نام متدها اضافه کنید. نحوه استفاده از آن به شکل زیر است:
var path=require("path");
path.exists('filepath",(status)=>
{
....
});
var status=path.existsSync("file");
//===============================
var fs=require("fs");
fs.exists('filepath",(status)=>
{
....
});
var status=path.existsSync("file");
ولی بهتر است بدانید که تاریخ انقضای دو دستور بالا سر آمده است و الان از دستور fs.stat استفاده می‌شود. این متد هم به دو شکل همزمان و غیرهمزمان وجود دارد:
fs.stat('foo.txt', function(err, stat) {
    if(err == null) {
        console.log('فایل موجوده');
    } else if(err.code == 'ENOENT') {
        // فایل وجود نداره
        fs.writeFile('log.txt', 'Some log\n');
    } else {
        //خطای دیگری رخ داده است
    }
});
برای استفاده از حالت همزمان هم کد را به شکل زیر بنویسید:
try {
  stats = fs.statSync(path);
  console.log("File exists.");
}
catch (e) {
  console.log("File does not exist.");
}
سپس کد زیر را در فرآیند اصلی وارد می‌کنیم:
const fs = require('fs');
const sql = require('sql.js');

  dbPath = './mydb.sqlite';
  dbExists=false;

try {
  dbExists = fs.statSync(dbPath);
}
catch (e) {
}

if(!dbExists)
{
  //create Database
var sqlStr=fs.readFileSync("./sql.txt");
var db = new sql.Database();
db.run(String(sqlStr));

//write to disk
var data=db.export();
var buffer=new Buffer(data);
fs.writeFileSync(dbPath,buffer);
}
else{
  var buffer = fs.readFileSync(dbPath);
  var db = new sql.Database(buffer);
}
ابتدا بررسی می‌کنیم که آیا فایلی با نام test.sqlite در مسیر جاری است یا خیر. در صورتی که نباشد، کد داخل شرط اجرا می‌شود. در اولین خط شرط، فایلی را با نام sql.txt که شامل محتوای زیر است، می‌خوانیم:
CREATE TABLE numbers (
    id     INT          PRIMARY KEY
                        UNIQUE
                        NOT NULL,
    fname  VARCHAR (20) NOT NULL,
    lname  VARCHAR (30) NOT NULL,
    number VARCHAR (15) NOT NULL
);
insert into numbers values(1,'ali','yeganeh','03111223344');
insert into numbers values(2,'xxx','yyy','45454555');
سپس در خطوط بعدی دیتابیس جدیدی را ایجاد می‌کنیم و دستور sql را با متد run اجرا می‌کنیم. دستوراتی که متد run اجرا می‌کند، شامل خروجی نیستند. پس دستوراتی را که نیاز به خروجی ندارند، به این متد بسپارید. سپس از این دیتابیس، یک شیء خروجی را دریافت میکنم و با بافر کردن، آن را در یک فایل ذخیره می‌کنیم. در صورتی هم که از قبل دیتابیس وجود داشته باشد، بافر خوانده شده را مستقیما به سازنده Database می‌دهیم.
بعد از آن نیاز است تا دیتابیس در دسترس Render Process‌ها قرار بگیرد که در مقاله "شیوه کدنویسی در الکترون " در مورد global صحبت کرده‌ایم و نحوه استفاده از آن را فرا گرفتیم:
global.db=db;

در پایان اجرای برنامه لازم است که دیتابیس توسط دستور close بسته شود. سپس کد زیر را در رویداد windows-all-closed می‌نویسیم:
app.on('window-all-closed', () => {
  db.close();
  if (process.platform !== 'darwin') {
    app.quit();
  }
});
در این کد گفته‌ایم که موقعی که تمام پنجره‌های برنامه بسته شدند، دیتابیس را نیز ببند.
(چند مورد خارج از بحث): کد بعدی که مورد استفاده قرار گرفته است و در مقالات قبلی در مورد آن صحبت نکرده‌ایم این است که در سیستم‌های مک، وضعیت به این قرار است که اگر شما برنامه را ببیندید، آن برنامه بسته نشده و در پس زمینه فعال است و می‌توانید آن را از طریق dock اطراف صفحه، مجددا فعال کنید. ولی با نوشتن کد بالا، ما این وضعیت را اعلام کرده‌ایم که اگر تمامی پنجره‌ها بسته شدند، کل برنامه را ببند.

همچنین بسیار خوب است که کد زیر را هم همیشه اضافه کنید:
win.on('closed', () => {
    win = null;
  });
موقعی که پنجره مربوطه بسته شود، متغیری که به پنجره اشاره می‌کند، در حافظه می‌ماند. پس بهتر است که این مقدار حافظه را رها کنید تا Garbage Collector اقدام به حذف آن در حافظه کند.
پس اگر این کد را نوشتید، وضعیت سیستم عامل مک را به خاطر داشته باشید و مجبور هستید کد زیر را نیز اضافه کنید:
app.on('activate', () => {
  if (win === null) {
    createWindow();
  }
});
در این صورت اگر کاربر پنجره را از طریق Dock مجددا فعال کرد، پنجره برنامه شما نیز مجددا نمایش داده خواهدشد.

بعد از اینکه دیتابیس را به شیء global دادیم، در صفحه html کد زیر را وارد می‌کنیم:
<html>
  <head>
    <script src="./jquery.min.js"></script>
    <link href="./bootstrap-3.3.6-dist/css/bootstrap.min.css" rel="stylesheet"></link>
    <meta charset="utf-8">
    <title></title>
    <script>
    const {remote}=require("electron");
    let db=remote.getGlobal("db");
    </script>
  </head>
  <body>

<table id="people" class="table table-hover table-striped">
<th>
  <tr>
    <td>First Name</td>
    <td>last Name</td>
    <td>Phone Number</td>
  </tr>
</th>
<tbody>

</tbody>
</table>
  </body>
</html>
در این کد یک جدول داریم و قصد ما این است که آن دو سطر را که در ابتدا اضافه کردیم، در آن نمایش دهیم. چیزی که در این کدها قابل ملاحظه است، این است که ما از بسته‌های بوت استرپ و جی کوئری استفاده می‌کنیم. در ادامه کدهای زیر را در تگ اسکریپت  وارد می‌کنیم:
$(document).ready(()=>
{
  //show data
var tableBody=$("#people");

db.each("select * from numbers",(row)=>{
  let rowTemplate=`<tr><td>${row.fname}</td><td>${row.lname}</td><td>${row.number}</td></tr>`;
  tableBody.append(rowTemplate);
});
متد each برای مواقعی مناسب است که شما تعدادی سطر را برای بازگشت دارید و callback آن، به ازای هر سطر اجرا می‌شود و با استفاده از یک template string سطر سازی را انجام داده و آن را با استفاده از توابع جی کوئری به انتهای جدول اضافه می‌کنیم.
حال وقت آن رسیده است که خروجی کار را ببینیم. پس کد npm start را اجرا می‌کنیم. همانطور که می‌بینید خروجی به راحتی نمایش داده می‌شود. در مقاله بعدی بیشتر در این مورد صحبت می‌کنیم.
نظرات مطالب
آپلود فایل توسط فرم‌های پویای jqGrid
در رخداد UploadFile گزینه imageName مقدار null دارد و خروجی زیر بر می‌گردد:
 if (imageName == null)
     return Json(new { success = false }, JsonRequestBehavior.AllowGet);
که من طبق صحبت شما به صورت زیر تغییرش دادم:
 return Json(new { success = false }, "text/html", JsonRequestBehavior.AllowGet);
حالا اون خطا برطرف شده ولی imageName به server ارسال نمیشه (null) 
مطالب
WF:Windows Workflow #۶
در این قسمت به تکمیل مثالی که در قسمت قبل زده شد پرداخته می‌شود و همچنین کنترل‌های Foreach , Try Catch نیز بررسی خواهند شد.
در ابتدا دو کلاس به نام‌های ItemInfo و OutOfStockException  را به برنامه اضافه می‌کنیم. کلاس اول برای ذخیره سازی مشخصات هر سفارش و کلاس دیگر برای مدیریت خطا‌ها می‌باشد.
public class ItemInfo
    {
        public string ItemCode { get; set; } 
        public string Description { get; set; } 
        public decimal Price { get; set; }
    }

public class OutOfStockException : Exception
    {
        public OutOfStockException() 
            : base() 
        { 

        }

        public OutOfStockException(string message) 
            : base(message) 
        { 

        }
    }
در Workflow مورد نظر که به نام OrderWF.xaml می‌باشد٬ پس از کنترل Assign که برای صفر کردن مقدار متغیر TotalAmount از آن استفاده می‌شود٬ یک کنترل ForEach را به Flow جاری اضافه می‌کنیم. این کنترل دارای دو خاصیت به نام‌های Type Arguments و Values می‌باشد. اولین خاصیت که مقدار پیش فرض آن، مقدار عددی Int32 است٬ برای مشخص کردن نوع متغییر حلقه و دیگری برای مشخص کردن نوع منبع داده حلقه تعریف شده‌اند.

همانطور که در شکل بالا مشخص می‌باشد٬ Type Arguments حلقه برابر با کلاس OrderItem می‌باشد. Values هم برابر با OrderInfo.Items است. از این جهت نوع حلقه  را از جنس کلاس OrderItem مشخص کرد‌‌ه‌ایم تا کنترل بر روی مقادیر Items اجرا شود (لیستی از کلاس مورد نظر).
حال همانند شکل بالا، در قسمت Body کنترل ForEach، یک کنترل Sequence را ایجاد کرده و سپس برای اینکه کنترل LookupItem را ایجاد کنیم٬ ابتدا باید یک Code Activity را به پروژه اضافه کنیم. به همین منظور پروژه جاری را انتخاب کرده و یک Code Activity به آن اضافه و نام آن را LookupItem  می‌گذاریم. سپس کد زیر را به آن اضافه می‌کنیم:
 public sealed class LookupItem : CodeActivity
    {
        // Define an activity input argument of type string
        public InArgument<string> ItemCode { get; set; }         
        public OutArgument<ItemInfo> Item { get; set; }

        // If your activity returns a value, derive from CodeActivity<TResult>
        // and return the value from the Execute method.
        protected override void Execute(CodeActivityContext context)
        {
            // Obtain the runtime value of the Text input argument
            ItemInfo i = new ItemInfo();             
            i.ItemCode = context.GetValue<string>(ItemCode);
            switch (i.ItemCode)
            {

                case "12345": 
                    i.Description = "Widget"; 
                    i.Price = (decimal)10.0; 
                    break;
                case "12346": 
                    i.Description = "Gadget"; 
                    i.Price = (decimal)15.0; 
                    break;
                case "12347": 
                    i.Description = "Super Gadget"; 
                    i.Price = (decimal)25.0; break;
            }
     
            context.SetValue(this.Item, i);
            
        }
    }
در این کد، دو متغییر تعریف شده‌اند؛ یکی از نوع رشته بوده و از طریق آن، دستور Switch تصمیم می‌گیرد که کلاس ItemInfo را با چه مقادیری پرکند. متغییر دیگر از نوع کلاس  ItemInfo می‌باشد و برای گرفتن مقدار کلاس از دستور Switch تعریف شده است.
حال برای اینکه بتوانیم از Code Activity مورد نظر استفاده کنیم٬ ابتدا باید پروژه را یکبار Build کنیم. اکنون در قسمت Toolbox یک٬ Tab ایی به نام پروژه ایجاد شده است و در آن یک کنترل به نام  LookupItem  موجود می‌باشد. آن را گرفته و به درون Sequence انتقال می‌دهیم.
سپس برای مقدار دادن به متغیر‌های تعریف شده در Code Activity، کنترل LookupItem را انتخاب کرده و در قسمت Properties به خصوصیت ItemCode، کد زیر را اضافه می‌کنیم:
item.ItemCode

نکته
: از کلاس Code Activity برای ارسال و دریافت مقادیر به درون Workflow استفاده می‌شود.

 Try Catch 
از این کنترل برای مدیریت خطا‌ها استفاده می‌شود.
ابتدا یک کنترل Try Catch را به Workflow اضافه کرده، مانند شکل زیر:

در بدنه Try می‌توان از کنترل‌های مورد نظر استفاده کنیم و همانطور که در شکل بالا مشخص است٬ از کنترل Throw برای ایجاد خطا استفاده شده‌است. کنترل جاری را انتخاب کرده و از قسمت Properties در خاصیت Exception کد زیر را اضافه می‌کنیم:
new OutOfStockException("Item Code"+item.ItemCode)
این  دستور باعث ایجاد یک خطا از نوع کلاس OutOfStockException می‌شود. برای کنترل خطای مورد نظر در قسمت Catches مانند شکل زیر عمل می‌کنیم.

نظرات مطالب
ساخت DropDownList های مرتبط به کمک jQuery Ajax در MVC
سلام ،با تشکر از آموزشهای فوق العاده تون از درس1 تا این قسمت‏ها
من 2 ماهی هست که شروع کردم به یادگیری mvc و امروز رسیدم به این قسمت ، برنامه رو مثل شما نوشتم و یه کم مشکل دارم: قسمتی که باید dropdownList دوم که گروه است رو پر کنه کار نمی‌کنه! وارد کد jquery میشه و حتی متد SelectCategory رو هم فراخوانی میکنه اما dropdownList دومم رو خالی بر میگردونه!
نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
تکمیل JavaScript Isolation در Blazor 6x

همانطور که کمی بالاتر نیز عنوان شد، CSS Isolation جزئی از تازه‌های Blazor 5x بود؛ اکنون مشابه این قابلیت جهت فایل‌های js. به Blazor 6x هم اضافه شده‌است و روش کار با آن نیز همانند CSS Isolation است (البته این قابلیت از نگارش 5 هم وجود داشت؛ اما اینبار دیگر نیازی به تعریف فایل ماژول آن در wwwroot نیست). یعنی اگر کامپوننت ما در مسیر Pages/Panel.razor قرار داشته باشد، می‌توان برای این تک فایل، فایل js. متناظری را به نام Pages/Panel.razor.js تعریف کرد؛ با الگوی Component>.razor.js>. سپس اگر محتوی این فایل js. به صورت زیر باشد:
export function error(){
    alert('oops, an error');
}
روش فراخوانی آن در همان کامپوننت به صورت زیر خواهد بود (یعنی در اصل دیگر مهم نیست که این فایل js. کجا قرار می‌گیرد، هنگام publish به خروجی کپی خواهد شد):
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./Panel.razor.js");
await module.InvokeVoidAsync("error");
مزیت اینکار، نزدیک نگه داشتن static assets یک کامپوننت، در کنار آن است و به این ترتیب قابل درک‌تر کردن برنامه:
Pages/Panel.razor
Pages/Panel.razor.js
Pages/Panel.razor.css
به علاوه JS Isolation دو مزیت دیگر را هم به همراه دارد:
- دیگر نیازی به تعریف توابع و متدهای جاوا اسکریپتی در global namespace نیست (این متدها و اشیاء، به شیء سراسری window اضافه نمی‌شوند). Isolation در اینجا در اصل به معنای امکان استفاده‌ی از JavaScript modules است.
- استفاده کنندگان از پروژه‌های کتابخانه‌ای دیگر نیازی به الحاق دستی این فایل‌ها ندارند. منظور از الحاق دستی یا ذکر src تگ script اضافه شده به index.html، در مطلب تولید کتابخانه‌های Razor توضیح داده شده‌است و از الگوی content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)}_/. جهت مشخص سازی نام نهایی فایل js. به همراه کتابخانه، پیروی می‌کند. البته اگر نیاز است این نوع فایل‌ها در کتابخانه‌ها مستقیما استفاده شوند، باید مسیر فوق در کدها حتما ذکر شود:
_module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/RazorClassLibrary/componentName.razor.js");

یک نکته: روش صحیح کار با ماژول‌ها همانطور که در نکات فوق نیز بررسی شد، به صورت زیر است و باید در OnAfterRenderAsync شروع شده و سپس در آخر کار Dispose شوند:
export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}