مقدمه
SQL Server، با هر تقاضا به عنوان یک واحد مستقل رفتار میکند. در وضعیتهای پیچیده ای که فعالیتها توسط مجموعه ای از دستورات SQL انجام میشود، به طوری که یا همه باید اجرا شوند یا هیچکدام اجرا نشوند، این روش مناسب نیست. در چنین وضعیت هایی، نه تنها تقاضاهای موجود در یک دنباله به یکدیگر بستگی دارند، بلکه شکست یکی از تقاضاهای موجود در دنباله، به معنای این است که کل تقاضاهای موجود در دنباله باید لغو شوند، و تغییرات حاصل از تقاضاهای اجراشده در آن دنباله خنثی شوند تا بانک اطلاعاتی به حالت قبلی برگردد.1- تراکنش چیست؟
تراکنش شامل مجموعه ای از یک یا چند دستور SQL است که به عنوان یک واحد عمل میکنند. اگر یک دستور SQL در این واحد با موفقیت اجرا نشود، کل آن واحد خنثی میشود و داده هایی که در اجرای آن واحد تغییر کرده اند، به حالت اول برگردانده میشود. بنابراین تراکنش وقتی موفق است که هر یک از دستورات آن با موفقیت اجرا شوند. برای درک مفهوم تراکنش مثال زیر را در نظر بگیرید: سهامدار A در معامله ای 400 سهم از شرکتی را به سهامدار B میفروشد. در این سیستم، معامله وقتی کامل میشود که حساب سهامدار A به اندازه 400 بدهکار و حساب سهامدار B همزمان به اندازه 400 بستانکار شود. اگر هر کدام از این مراحل با شکست مواجه شود، معامله انجام نمیشود.2- خواص تراکنش
هر تراکنش دارای چهار خاصیت است (معروف به ACID) که به شرح زیر میباشند:2-1- خاصیت یکپارچگی (Atomicity)
یکپارچگی به معنای این است که تراکنش باید به عنوان یک واحد منسجم (غیر قابل تفکیک) در نظر گرفته شود. در مثال مربوط به مبادله سهام، یکپارچگی به معنای این است که فروش سهام توسط سهامدار A و خرید آن سهام توسط سهامدار B، مستقل از هم قابل انجام نیستند و برای این که تراکنش کامل شود، هر دو عمل باید با موفقیت انجام شوند.اجرای یکپارچه، یک عمل "همه یا هیچ" است. در عملیات یکپارچه، اگر هر کدام از دستورات موجود در تراکنش با شکست مواجه شوند، اجرای تمام دستورات قبلی خنثی میشود تا به جامعیت بانک اطلاعاتی آسیب نرسد.
2-2- خاصیت سازگاری (Consistency)
سازگاری زمانی وجود دارد که هر تراکنش، سیستم را در یک حالت سازگار قرار دهد (چه تراکنش به طور کامل انجام شود و چه در اثر وجود خطایی خنثی گردد). در مثال مبادله سهام، سازگاری به معنای آن است که هر بدهکاری مربوط به حساب فروشنده، موجب همان میزان بستانکاری در حساب خریدار میشود.در SQL Server، سازگاری با راهکار ثبت فایل سابقه انجام میگیرد که تمام تغییرات را در بانک اطلاعاتی ذخیره میکند و جزییات را برای ترمیم تراکنش ثبت مینماید. اگر سیستم در اثنای اجرای تراکنش خراب شود، فرآیند ترمیم SQL Server با استفاده از این اطلاعات، تعیین میکند که آیا تراکنش با موفقیت انجام شده است یا خیر، و در صورت عدم موفقیت آن را خنثی میکند. خاصیت سازگاری تضمین میکند که بانک اطلاعاتی هیچگاه تراکنشهای ناقص را نشان نمیدهد.
2-3- خاصیت تفکیک (Isolation)
تفکیک موجب میشود هر تراکنش در فضای خودش و جدا از سایر تراکنشهای دیگری که در سیستم انجام میگیرد، اجرا شود و نتایج هر تراکنش فقط در صورت کامل شدن آن قابل مشاهده است. اگر چندین تراکنش همزمان در سیستم در حال اجرا باشند، اصل تفکیک تضمین میکند که اثرات یک تراکنش تا کامل شدن آن، قابل مشاهده نیست. در مثال مربوط به مبادله سهام، اصل تفکیک به معنای این است که تراکنش بین دو سهامدار، مستقل از تمام تراکنشهای دیگری است که در سیستم به مبادله سهام میپردازند و اثر آن وقتی برای افراد قابل مشاهده است که آن تراکنش کامل شده باشد. این اصل در مواردی که سیستم همزمان از چندین کاربر پشتیبانی میکند، مفید است.2-4- پایداری (Durability)
پایداری به معنای این است که تغییرات حاصل از نهایی شدن تراکنش، حتی در صورت خرابی سیستم نیز پایدار میماند. اغلب سیستمهای مدیریت بانک اطلاعاتی رابطه ای، از طریق ثبت تمام فعالیتهای تغییر دهندهی دادهها در بانک اطلاعاتی، پایداری را تضمین میکنند. در صورت خرابی سیستم یا رسانه ذخیره سازی داده ها، سیستم قادر است آخرین بهنگام سازی موفق را هنگام راه اندازی مجدد، بازیابی کند. در مثال مربوط به مبادله سهام، پایداری به معنای این است که وقتی انتقال سهام از سهامدار A به B با موفقیت انجام گردید، حتی اگر سیستم بعداً خراب شد، باید نتیجهی آن را منعکس سازد.3- مشکلات همزمانی(Concurrency Effects):
3-1- Dirty Read:
زمانی روی میدهد که تراکنشی رکوردی را میخواند، که بخشی از تراکنشی است که هنوز تکمیل نشده است، اگر آن تراکنش Rollback شود اطلاعاتی از بانک اطلاعاتی دارید که هرگز روی نداده است.اگر سطح جداسازی تراکنش (پیش فرض) Read Committed باشد، این مشکل بوجود نمیآید.
3-2- Non-Repeatable Read:
زمانی ایجاد میشود که رکوردی را دو بار در یک تراکنش میخوانید و در این اثنا یک تراکنش مجزای دیگر دادهها را تغییر میدهد. برای پیشگیری از این مسئله باید سطح جداسازی تراکنش برابر با Repeatable Read یا Serializable باشد.3-3- Phantoms:
با رکوردهای مرموزی سروکار داریم که گویی تحت تاثیر عبارات Update و Delete صادر شده قرار نگرفته اند. به طور خلاصه شخصی عبارت Insert را درست در زمانی که Update مان در حال اجرا بوده انجام داده است، و با توجه به اینکه ردیف جدیدی بوده و قفلی وجود نداشته، به خوبی انجام شده است. تنها چاره این مشکل تنظیم سطح Serializable است و در این صورت بهنگام رسانیهای جداول نباید درون بخش Where قرار گیرد، در غیر این صورت Lock خواهند شد.3-4- Lost Update:
زمانی روی میدهد که یک Update به طور موفقیت آمیزی در بانک اطلاعاتی نوشته میشود، اما به طور اتفاقی توسط تراکنش دیگری بازنویسی میشود. راه حل این مشکل بستگی به کد شما دارد و بایست به نحوی تشخیص دهید، بین زمانی که دادهها را میخوانید و زمانی که میخواهید آنرا بهنگام کنید، اتصال دیگری رکورد شما را بهنگام کرده است.4- منابع قابل قفل شدن
6 منبع قابل قفل شدن برای SQL Server وجود دارد و آنها سلسله مراتبی را تشکیل میدهند. هر چه سطح قفل بالاتر باشد، Granularity کمتری دارد. در ترتیب آبشاری Granularity عبارتند از:• Database: کل بانک اطلاعاتی قفل شده است، معمولاً طی تغییرات Schema بانک اطلاعاتی روی میدهد.
• Table: کل جدول قفل شده است، شامل همه اشیای مرتبط با جدول.
• Extent: کل Extent (متشکل از هشت Page) قفل شده است.
• Page: همه دادهها یا کلیدهای Index در آن Page قفل شده اند.
• Key: قفلی در کلید مشخصی یا مجموعه کلید هایی Index وجود دارد. ممکن است سایر کلیدها در همان Index Page تحت تاثیر قرار نگیرند.
• (Row or Row Identifier (RID: هر چند قفل از لحاظ فنی در Row Identifier قرار میگیرد ولی اساساً کل ردیف را قفل میکند.
5- تسریع قفل (Lock Escalation) و تاثیرات قفل روی عملکرد
اگر تعداد آیتمهای قفل شده کم باشد نگهداری سطح بهتری از Granularity (مثلاً RID به جای Page) معنی دار است. هرچند با افزایش تعداد آیتمهای قفل شده، سربار مرتبط با نگهداری آن قفلها در واقع باعث کاهش عملکرد میشود، و میتواند باعث شود قفل به مدت طولانیتری در محل باشد(هر چه قفل به مدت طولانیتری در محل باشد، احتمال این که شخصی آن رکورد خاص را بخواهد بیشتر است).هنگامی که تعداد قفل نگهداری شده به آستانه خاصی برسد آن گاه قفل به بالاترین سطح بعدی افزایش مییابد و قفلهای سطح پایینتر نباید به شدت مدیریت شوند (آزاد کردن منابع و کمک به سرعت در مجادله).
توجه شود که تسریع مبتنی بر تعداد قفل هاست و نه تعداد کاربران.
6- حالات قفل (Lock Modes):
همانطور که دامنه وسیعی از منابع برای قفل شدن وجود دارد، دامنه ای از حالات قفل نیز وجود دارد.6-1- (Shared Locks (S:
زمانی استفاده میشود، که فقط باید دادهها را بخوانید، یعنی هیچ تغییری ایجاد نخواهید کرد. Shared Lock با سایر Shared Lockهای دیگر سازگار است، البته قفلهای دیگری هستند که با Shared Lock سازگار نیستند. یکی از کارهایی که Shared Lock انجام میدهد، ممانعت از انجام Dirty Read از طرف کاربران است.6-2- (Exclusive Locks (X:
این قفلها با هیچ قفل دیگری سازگار نیستند. اگر قفل دیگری وجود داشته باشد، نمیتوان به Exclusive Lock دست یافت و همچنین در حالی که Exclusive Lock فعال باشد، به هر قفل جدیدی از هر شکل اجازه ایجاد شدن در منبع را نمیدهند.این قفل از اینکه دو نفر همزمان به حذف کردن، بهنگام رسانی و یا هر کار دیگری مبادرت ورزند، پیشگیری میکند.
6-3- (Update Locks (U:
این قفل ها نوعی پیوند میان Shared Locks و Exclusive Locks هستند.برای انجام Update باید بخش Where را (در صورت وجود) تایید اعتبار کنید، تا دریابید فقط چه ردیف هایی را میخواهید بهنگام رسانی کنید. این بدان معنی است که فقط به Shared Lock نیاز دارید، تا زمانی که واقعاً بهنگام رسانی فیزیکی را انجام دهید. در زمان بهنگام سازی فیزیکی نیاز به Exclusive Lock دارید.
Update Lock نشان دهنده این واقعیت است که دو مرحله مجزا در بهنگام رسانی وجود دارد، Shared Lock ای دارید که در حال تبدیل شدن به Exclusive Lock است. Update Lock تمامی Update Lockهای دیگر را از تولید شدن باز میدارند، و همچنین فقط با Shared Lock و Intent Shared Lockها سازگار هستند.
6-4- Intent Locks:
با سلسله مراتب شی سر و کار دارد. بدون Intent Lock، اشیای سطح بالاتر نمیدانند چه قفلی را در سطح پایینتر داشته اید. این قفلها کارایی را افزایش میدهند و 3 نوع هستند:6-4-1- (Intent Shared Lock (IS:
Shared Lock در نقطه پایینتری در سلسله مراتب، تولید شده یا در شرف تولید است. این نوع قفل تنها به Table و Page اعمال میشود.6-4-2- (Intent Exclusive Lock (IX:
همانند Intent Shared Lock است اما در شرف قرار گرفتن در آیتم سطح پایینتر است.6-4-3- (Shared With Intent Exclusive (SIX:
Shared Lock در پایین سلسله مراتب شی تولید شده یا در شرف تولید است اما Intent Lock قصد اصلاح دادهها را دارد بنابراین در نقطه مشخصی تبدیل به Intent Exclusive Lock میشود.6-5- Schema Locks:
به دو شکل هستند:6-5-1- (Schema Modification Lock (Sch-M:
تغییر Schema به شی اعمال شده است. هیچ پرس و جویی یا سایر عبارتهای Create، Alter و Drop نمیتوانند در مورد این شی در مدت قفل Sch-M اجرا شوند. با همه حالات قفل ناسازگار است.6-5-2- (Schema Stability Lock (Sch-S:
بسیار شبیه به Shared Lock است، هدف اصلی این قفل پیشگیری از Sch-M است وقتی که قبلاً قفل هایی برای سایر پرس و جو-ها (یا عبارتهای Create، Alter و Drop) در شی فعال شده اند. این قفل با تمامی انواع دیگر قفل سازگار است به جز با Sch-M.6-6- (Bulk Update Locks (BU:
این قفلها بارگذاری موازی دادهها را امکان پذیر میکنند، یعنی جدول در مورد هر فعالیت نرمال (عبارات T-SQL) قفل میشود، اما چندین عمل bcp یا Bulk Insert را میتوان در همان زمان انجام داد. این قفل فقط با Sch-S و سایر قفل هایBU سازگار است.7- سطوح جداسازی (Isolation Level):
7-1- Read Committed (وضعیت پیش فرض):
با Read Committed همه Shared Lockهای ایجاد شده، به محض اینکه عبارت ایجاد کننده آنها تکمیل شود، به طور خودکار آزاد میشوند. به طور خلاصه قفلهای مرتبط با عبارت Select به محض تکمیل عبارت Select آزاد میشوند و SQL Server منتظر پایان تراکنش نمیماند. اگر تراکنش پرس و جویی را انجام میدهد که دادهها را اصلاح میکند (Insert، Delete و Update) قفلها برای مدت تراکنش نگه داشته میشوند.با این سطح پیش فرض، میتوانید مطمئن شوید جامعیت کافی برای پیشگیری از Dirty Read دارید، اما همچنان Phantoms و Non-Repeatable Read میتواند روی دهد.
7-2- Read Uncommitted:
خطرناکترین گزینه از میان تمامی گزینهها است، اما بالاترین عملکرد را به لحاظ سرعت دارد. در واقع با این تنظیم سطح تجربه همه مسائل متعدد هم زمانی مانند Dirty Read امکان پذیر است. در واقع با تنظیم این سطح به SQL Server اعلام میکنیم هیچ قفلی را تنظیم نکرده و به هیچ قفلی اعتنا نکند، بنابراین هیچ تراکنش دیگری را مسدود نمیکنیم.میتوانید همین اثر Read Uncommitted را با اضافه کردن نکته بهینه ساز NOLOCK در پرس و جوها بدست آورید.
7-3- Repeatable Read:
سطح جداسازی را تا حدودی افزایش میدهد و سطح اضافی محافظت همزمانی را با پیشگیری از Dirty Read و همچنین Non-Repeatable Read فراهم میکند.پیشگیری از Non-Repeatable Read بسیار مفید است اما حتی نگه داشتن Shared Lock تا زمان پایان تراکنش میتواند دسترسی کاربران به اشیا را مسدود کند، بنابراین به بهره وری لطمه وارد میکند.
نکته بهینه ساز برای این سطح REPEATEABLEREAD است.
7-4- Serializable:
این سطح از تمام مسائل هم زمانی پیشگیری میکند به جز برای Lost Update.این تنظیم سطح به واقع بالاترین سطح آنچه را که سازگاری نامیده میشود، برای پایگاه داده فراهم میکند. در واقع فرآیند بهنگام رسانی برای کاربران مختلف به طور یکسان عمل میکند به گونه ای که اگر همه کاربران یک تراکنش را در یک زمان اجرا میکردند، این گونه میشد « پردازش امور به طور سریالی».
با استفاده از نکته بهینه ساز SERIALIZABLE یا HOLDLOCK در پرس و جو شبیه سازی میشود.
7-5- Snapshot:
جدترین سطح جداسازی است که در نسخه 2005 اضافه شد، که شبیه ترکیبی از Read Committed و Read Uncommitted است. به طور پیش فرض در دسترس نیست، در صورتی در دسترس است که گزینه ALLOW_SNAPSHOT_ISOLATION برای بانک اطلاعاتی فعال شده باشد.(برای هر بانک اطلاعاتی موجود در تراکنش)Snapshot مشابه Read Uncommitted هیچ قفلی ایجاد نمیکند. تفاوت اصلی آنها در این است که تغییرات صورت گرفته در بانک اطلاعاتی را در زمانهای متفاوت تشخیص میدهند. هر تغییر در بانک اطلاعاتی بدون توجه به زمان یا Commit شدن آن، توسط پرس و جو هایی که سطح جداسازی Read Uncommitted را اجرا میکنند، دیده میشود. با Snapshot فقط تغییراتی که قبل از شروع تراکنش، Commit شده اند، مشاهده میشود.
از شروع تراکنش Snapshot، تمامی دادهها دقیقاً مشاهده میشوند، زیرا در شروع تراکنش Commit شده اند.
نکته: در حالی که Snapshot توجهی به قفلها و تنظیمات آنها ندارد، یک حالت خاص وجود دارد. چنانچه هنگام انجام Snapshot یک عمل Rollback (بازیافت) بانک اطلاعاتی در جریان باشد، تراکنش Snapshot قفلهای خاصی را برای عمل کردن به عنوان یک مکان نگهدار و سپس انتظار برای تکمیل Rollback تنظیم میکند. به محض تکمیل Rollback، قفل حذف شده و Snapshot به طور طبیعی به جلو حرکت خواهد کرد.
عموما در برنامههای وب مرسوم است که پیغام به کاربر را در همان لابلای html صفحه نمایش میدهند. مثلا یک برچسب و سپس تنظیم متن آن در کد برنامه به صورت پویا.
با استفاده از پلاگینهای jQuery اینکار را به صورت شکیلتری میتوان انجام داد. برای مثال:
پلاگین کم حجمی برای این منظور موجود است به نام jQuery Notice (یکی از چند ده نمونه موجود)
<script type="text/javascript">
$(document).ready(function()
{
jQuery.noticeAdd({
text: 'پیغامی به کاربر',
stay: false
});
});
</script>
<div id="myElement" style="position: absolute">This stays at the top</div>
با استفاده از jQuery اینکار به صورت زیر قابل انجام است:
<script type="text/javascript">
$(document).ready(function()
{
$(window).scroll(function() {
$('#myElement').css('top', $(this).scrollTop() + "px");
});
});
</script>
ولی این روش جهت نمایش پیغامی پویا به کاربر مشکل دارد.
نیاز است به ازای هر پیغام پویا یکبار به نحوی این اسکریپت به صفحه تزریق شود که روش انجام کار در ASP.Net به صورت زیر میتواند باشد:
using System;
using System.Web.UI;
using System.Web;
public class CAddJqueryNotice
{
/// <summary>
/// نمایش یک پیغام بر اساس پلاگین نوتیس
/// </summary>
/// <param name="title">عنوان</param>
/// <param name="msg">پیغام</param>
/// <param name="rtl">راست به چپ؟</param>
/// <param name="duration">مدت زمان نمایش</param>
/// <param name="autoHide">به صورت خودکار بسته شود؟</param>
public static void Show(string title, string msg, bool rtl, int duration, bool autoHide)
{
string scriptBlock
= string.Format(@"<script type=""text/javascript"">
$(document).ready(function() {{
jQuery.noticeAdd({{
text: '<b>{0}</b><br/><div align=left dir={1}>{2}</div>',
stay: {3},
stayTime: {4}
}});
}});
</script>",
title,
(rtl ? "rtl" : "ltr"),
msg,
(autoHide ? "false" : "true"),
duration);
if (HttpContext.Current == null || HttpContext.Current.Handler == null) return;
Page page = HttpContext.Current.Handler as Page;
if (page != null)
page.ClientScript.RegisterStartupScript(
page.GetType(),
"script" + new Guid().ToString("N"),
scriptBlock,
false);
}
}
برای آزمایش آن یک دکمه را در صفحه قرار داده و در روال رخداد گردان کلیک آن کد زیر را اضافه کنید:
CAddJqueryNotice.Show( "لطفا دوباره سعی کنید", "مشکلی رخ داده است", true, 2000, true);
بدیهی است قبل از استفاده از کد فوق، باید چند سطر زیر را به هدر master page سایت خود اضافه کنید:
<script src="jquery-1.3.2.js" type="text/javascript"></script>
<link href="jquery.notice.css" type="text/css" media="screen" rel="stylesheet" />
<script src="jquery.notice.js" type="text/javascript"></script>
انجام اینکار نیز با دو سطر کد نویسی توسط jQuery قابل انجام است.
در ادامه خروجی رندر شده فیلدی را که پرکردن آن الزامی است مشاهده میکنید:
<div class="editor-label"> <label for="Title">عنوان</label> </div> <div class="editor-field"> <input class="text-box single-line" data-val="true" data-val-required="*" id="Title" name="Title" type="text" value="" /> <span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span> </div>
$(function() { $('[data-val-required]').after('<span class="required-indicator"> (*)</span>'); });
.required-indicator { color: red; font-size: 1.2em; font-weight: bold; }
Create Table TestSort (ID int identity(1,1), Name nvarchar(30), Color nvarchar(15) )
Insert into TestSort (Name,color) Values ('Adjustable Race',null) ,('Bearing Ball',null) ,('Headset Ball Bearings',null) ,('LL Crankarm','Black') ,('ML Crankarm','Black') ,('Chainring','Black') ,('Front Derailleur Cage','Silver') ,('Front Derailleur Linkage','Silver') ,('Lock Ring','Silver') ,('HL Road Frame - Red, 62','Red') ,('HL Road Frame - Red, 48','Red') ,('LL Road Frame - Red, 44','Red') ,('Full-Finger Gloves, M','RED') ,('Road-550-W Yellow, 38','Yellow') ,('Road-550-W Yellow, 40','Yellow') ,('Road-550-W Yellow, 42','Yellow') ,('Classic Vest, S','Blue') ,('Classic Vest, M','Blue') ,('Classic Vest, L','Blue')
Select t.ID,t.Name,t.Color from TestSort as t order by t.Color
Select t.ID,t.Name,t.Color from TestSort as t Where t.color is not null order by Case t.Color When 'Red' Then Null Else t.color End;
Select * into Simulation from (Select t.ID,t.Name,t.Color, Case t.Color When 'Red' Then Null Else t.color End RedNull from TestSort as t Where t.color is not null) A
Select * from Simulation order by Rednull
ASP.NET MVC #5
بررسی نحوه انتقال اطلاعات از یک کنترلر به Viewهای مرتبط با آن
در ASP.NET Web forms در فایل code behind یک فرم مثلا میتوان نوشت Label1.Text و سپس مقداری را به آن انتساب داد. اما اینجا به چه ترتیبی میتوان شبیه به این نوع عملیات را انجام داد؟ با توجه به اینکه در کنترلرها هیچ نوع ارجاع مستقیمی به اشیاء رابط کاربری وجود ندارد و این دو از هم مجزا شدهاند.
در پاسخ به این سؤال، همان مثال ساده قسمت قبل را ادامه میدهیم. یک پروژه جدید خالی ایجاد شده است به همراه HomeController ایی که به آن اضافه کردهایم. همچنین مطابق روشی که ذکر شد، View ایی به نام Index را نیز به آن اضافه کردهایم. سپس برای ارسال اطلاعات از یک کنترلر به View از یکی از روشهای زیر میتوان استفاده کرد:
الف) استفاده از اشیاء پویا
ViewBag یک شیء dynamic است که در دات نت 4 امکان تعریف آن میسر شده است. به این معنا که هر نوع خاصیت دلخواهی را میتوان به این شیء انتساب داد و سپس این اطلاعات در View نیز قابل دسترسی و استخراج خواهد بود. مثلا اگر در اینجا به شیء ViewBag، خاصیت دلخواه Country را اضافه کنیم و سپس مقداری را نیز به آن انتساب دهیم:
using System.Web.Mvc;
namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Country = "Iran";
return View();
}
}
}
این اطلاعات در View مرتبط با اکشنی به نام Index به نحو زیر قابل بازیابی خواهد بود (نحوه اضافه کردن View متناظر با یک اکشن یا متد را هم در قسمت قبل با تصویر مرور کردیم):
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<p>
Country : @ViewBag.Country
</p>
در این مثال، @ در View engine جاری که Razor نام دارد، به این معنا میباشد که این مقداری است که میخواهم دریافت کنی (ViewBag.Country) و سپس آنرا در حین پردازش صفحه نمایش دهی.
ب) انتقال اطلاعات یک شیء کامل و غیر پویا به View
هر پروژه جدید MVC به همراه پوشهای به نام Models است که در آن میتوان تعاریف اشیاء تجاری برنامه را قرار داد. در پروژه جاری، یک کلاس ساده را به نام Employee به این پوشه اضافه میکنیم:
namespace MvcApplication1.Models
{
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}
اکنون برای نمونه یک وهله از این شیء را در متد Index ایجاد کرده و سپس به view متناظر با آن ارسال میکنیم (در قسمت return View کد زیر مشخص است). بدیهی است این وهله سازی در عمل میتواند از طریق دسترسی به یک بانک اطلاعاتی یا یک وب سرویس و غیره باشد.
using System.Web.Mvc;
using MvcApplication1.Models;
namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Country = "Iran";
var employee = new Employee
{
Email = "name@site.com",
FirstName = "Vahid",
LastName = "N."
};
return View(employee);
}
}
}
امضاهای متفاوت (overloads) متد کمکی View هم به شرح زیر هستند:
ViewResult View(Object model)
ViewResult View(string viewName, Object model)
ViewResult View(string viewName, string masterName, Object model)
اکنون برای دسترسی به اطلاعات این شیء employee در View متناظر با این متد، چندین روش وجود دارد:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
Country: @ViewBag.Country <br />
FirstName: @Model.FirstName
</div>
میتوان از طریق شیء استاندارد دیگری به نام Model (که این هم یک شیء dynamic است مانند ViewBag قسمت قبل)، به خواص شیء یا مدل ارسالی به View جاری دسترسی پیدا کرد که یک نمونه از آنرا در اینجا ملاحظه میکنید.
روش دوم، بر اساس تعریف صریح نوع مدل است به نحو زیر:
@model MvcApplication1.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
Country: @ViewBag.Country
<br />
FirstName: @Model.FirstName
</div>
در اینجا در مقایسه با قبل، تنها یک سطر به اول فایل View اضافه شده است که در آن نوع شیء Model تعیین میگردد (کلمه model هم در اینجا با حروف کوچک شروع شده است). به این ترتیب اینبار اگر سعی کنیم به خواص این شیء دسترسی پیدا کنیم، Intellisense ویژوال استودیو ظاهر میشود. به این معنا که شیء Model بکارگرفته شده اینبار دیگر dynamic نیست و دقیقا میداند که چه خواصی را باید پیش از اجرای برنامه در اختیار استفاده کننده قرار دهد.
به این روش، روش Strongly typed view هم گفته میشود؛ چون View دقیقا میداند که چون نوعی را باید انتظار داشته باشد؛ تحت نظر کامپایلر قرار گرفته و همچنین Intellisense نیز برای آن مهیا خواهد بود.
به همین جهت این روش Strongly typed view، در بین تمام روشهای مهیا، به عنوان روش توصیه شده و مرجح مطرح است.
به علاوه استفاده از Strongly typed views یک مزیت دیگر را هم به همراه دارد: فعال شدن یک code generator توکار در VS.NET به نام scaffolding. یک مثال ساده:
تا اینجا ما اطلاعات یک کارمند را نمایش دادیم. اگر بخواهیم یک لیست از کارمندها را نمایش دهیم چه باید کرد؟
روش کار با قبل تفاوتی نمیکند. اینبار در return View ما، یک شیء لیستی ارائه خواهد شد. در سمت View هم با یک حلقه foreach کار نمایش این اطلاعات صورت خواهد گرفت. راه سادهتری هم هست. اجازه دهیم تا خود VS.NET، کدهای مرتبط را برای ما تولید کند.
یک کلاس دیگر به پوشه مدلهای برنامه اضافه کنید به نام Employees با محتوای زیر:
using System.Collections.Generic;
namespace MvcApplication1.Models
{
public class Employees
{
public IList<Employee> CreateEmployees()
{
return new[]
{
new Employee { Email = "name1@site.com", FirstName = "name1", LastName = "LastName1" },
new Employee { Email = "name2@site.com", FirstName = "name2", LastName = "LastName2" },
new Employee { Email = "name3@site.com", FirstName = "name3", LastName = "LastName3" }
};
}
}
}
سپس متد جدید زیر را به کنترلر Home اضافه کنید.
public ActionResult List()
{
var employeesList = new Employees().CreateEmployees();
return View(employeesList);
}
برای اضافه کردن View متناظر با آن، روی نام متد کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه ظاهر شده:
تیک مربوط به Create a strongly typed view را قرار دهید. سپس در قسمت Model class، کلاس Employee را انتخاب کنید (نه Employees جدید را، چون از آن میخواهیم به عنوان منبع داده لیست تولیدی استفاده کنیم). اگر این کلاس را مشاهده نمیکنید، به این معنا است که هنوز برنامه را یکبار کامپایل نکردهاید تا VS.NET بتواند با اعمال Reflection بر روی اسمبلی برنامه آنرا پیدا کند. سپس در قسمت Scaffold template گزینه List را انتخاب کنید تا Code generator توکار VS.NET فعال شود. اکنون بر روی دکمه Add کلیک نمائید تا View نهایی تولید شود. برای مشاهده نتیجه نهایی مسیر http://localhost/Home/List باید بررسی گردد.
ج) استفاده از ViewDataDictionary
ViewDataDictionary از نوع IDictionary با کلیدی رشتهای و مقداری از نوع object است. توسط آن شیءایی به نام ViewData در ASP.NET MVC به نحو زیر تعریف شده است:
public ViewDataDictionary ViewData { get; set; }
این روش در نگارشهای اولیه ASP.NET MVC بیشتر مرسوم بود. برای مثال:
using System;
using System.Web.Mvc;
namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["DateTime"] = "<br/>" + DateTime.Now;
return View();
}
}
}
و سپس جهت استفاده از این ViewData تعریف شده با کلید دلخواهی به نام DateTime در View متناظر با اکشن Index خواهیم داشت:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
DateTime: @ViewData["DateTime"]
</div>
یک نکته امنیتی:
اگر به مقدار انتساب داده شده به شیء ViewDataDictionary دقت کنید، یک تگ br هم به آن اضافه شده است. برنامه را یکبار اجرا کنید. مشاهده خواهید کرد که این تگ به همین نحو نمایش داده میشود و نه به صورت یک سطر جدید HTML . چرا؟ چون Razor به صورت پیش فرض اطلاعات را encode شده (فراخوانی متد Html.Encode در پشت صحنه به صورت خودکار) در صفحه نمایش میدهد و این مساله از لحاظ امنیتی بسیار عالی است؛ زیرا جلوی بسیاری از حملات cross site scripting یا XSS را خواهد گرفت.
احتمالا الان این سؤال پیش خواهد آمد که اگر «عالمانه» بخواهیم این رفتار نیکوی پیش فرض را غیرفعال کنیم چه باید کرد؟
برای این منظور میتوان نوشت:
@Html.Raw(myString)
و یا:
<div>@MvcHtmlString.Create("<h1>HTML</h1>")</div>
به این ترتیب خروجی Razor دیگر encode شده نخواهد بود.
د) استفاده از TempData
TempData نیز یک dictionary دیگر برای ذخیره سازی اطلاعات است و به نحو زیر در فریم ورک تعریف شده است:
public TempDataDictionary TempData { get; set; }
TempData در پشت صحنه از سشنهای ASP.NET جهت ذخیره سازی اطلاعات استفاده میکند. بنابراین اطلاعات آن در سایر کنترلرها و View ها نیز در دسترس خواهد بود. البته TempData یک سری تفاوت هم با سشن معمولی ASP.NET دارد:
- بلافاصله پس از خوانده شدن، حذف خواهد شد.
- پس از پایان درخواست از بین خواهد رفت.
هر دو مورد هم به جهت بالابردن کارآیی برنامههای ASP.NET MVC و مصرف کمتر حافظه سرور درنظر گرفته شدهاند.
البته کسانی که برای بار اول هست با ASP.NET مواجه میشوند، شاید سؤال بپرسند این مسایل چه اهمیتی دارد؟ پروتکل HTTP، ذاتا یک پروتکل «بدون حالت» است یا Stateless هم به آن گفته میشود. به این معنا که پس از ارائه یک صفحه وب توسط سرور، تمام اشیاء مرتبط با آن در سمت سرور تخریب خواهند شد. این مورد متفاوت است با برنامههای معمولی دسکتاپ که طول عمر یک شیء معمولی تعریف شده در سطح فرم به صورت یک فیلد، تا زمان باز بودن آن فرم، تعیین میگردد و به صورت خودکار از حافظه حذف نمیشود. این مساله دقیقا مشکل تمام تازه واردها به دنیای وب است که چرا اشیاء ما نیست و نابود شدند. در اینجا وب سرور قرار است به هزاران درخواست رسیده پاسخ دهد. اگر قرار باشد تمام این اشیاء را در سمت سرور نگهداری کند، خیلی زود با اتمام منابع مواجه میگردد. اما واقعیت این است که نیاز است یک سری از اطلاعات را در حافظه نگه داشت. به همین منظور یکی از چندین روش مدیریت حالت در ASP.NET استفاده از سشنها است که در اینجا به نحو بسیار مطلوبی، با سربار حداقل توسط TempData مدیریت شده است.
یک مثال کاربردی در این زمینه:
فرض کنید در متد جاری کنترلر، ابتدا بررسی میکنیم که آیا ورودی دریافتی معتبر است یا خیر. در غیراینصورت، کاربر را به یک View دیگر از طریق کنترلری دیگر جهت نمایش خطاها هدایت خواهیم کرد.
همین «هدایت مرورگر به یک View دیگر» یعنی پاک شدن و تخریب اطلاعات کنترلر قبلی به صورت خودکار. بنابراین نیاز است این اطلاعات را در TempData قرار دهیم تا در کنترلری دیگر قابل استفاده باشد:
using System;
using System.Web.Mvc;
namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult InsertData(string name)
{
// Check for input errors.
if (string.IsNullOrWhiteSpace(name))
{
TempData["error"] = "name is required.";
return RedirectToAction("ShowError");
}
// No errors
// ...
return View();
}
public ActionResult ShowError()
{
var error = TempData["error"] as string;
if (!string.IsNullOrWhiteSpace(error))
{
ViewBag.Error = error;
}
return View();
}
}
}
در همان HomeController دو متد جدید به نامهای InsertData و ShowError اضافه شدهاند. در متد InsertData ابتدا بررسی میشود که آیا نامی وارد شده است یا خیر. اگر خیر توسط متد RedirectToAction، کاربر به اکشن یا متد ShowError هدایت خواهد شد.
برای انتقال اطلاعات خطایی که میخواهیم در حین این Redirect نمایش دهیم نیز از TempData استفاده شده است.
بدیهی است برای اجرا این مثال نیاز است دو View جدید برای متدهای InsertData و ShowError ایجاد شوند (کلیک راست روی نام متد و انتخاب گزینه Add view برای اضافه کردن View مرتبط با آن اکشن).
محتوای View مرتبط با متد افزودن اطلاعات فعلا مهم نیست، ولی View نمایش خطاها در سادهترین حالت مثلا میتواند به صورت زیر باشد:
@{
ViewBag.Title = "ShowError";
}
<h2>Error</h2>
@ViewBag.Error
برای آزمایش برنامه هم مطابق مسیریابی پیش فرض و با توجه به قرار داشتن در کنترلری به نام Home، مسیر http://localhost/Home/InsertData ابتدا باید بررسی شود. چون آرگومانی وارد نشده، بلافاصله صفحه به آدرس http://localhost/Home/ShowError به صورت خودکار هدایت خواهد شد.
نکتهای تکمیلی در مورد Strongly typed viewها:
عنوان شد که Strongly typed view روش مرجح بوده و بهتر است از آن استفاده شود، زیرا اطلاعات اشیاء و خواص تعریف شده در یک View تحت نظر کامپایلر قرار میگیرند که بسیار عالی است. یعنی اگر در View بنویسم FirstName: @Model.FirstName1 چون FirstName1 وجود خارجی ندارد، برنامه نباید کامپایل شود. یکبار این را بررسی کنید. برنامه بدون مشکل کامپایل میشود! اما تنها در زمان اجرا است که صفحه زرد رنگ معروف خطاهای ASP.NET ظاهر میشود که چنین خاصیتی وجود ندارد (این حالت پیش فرض است؛ یعنی کامپایل یک View در زمان اجرا). البته این باز هم خیلی بهتر است از ViewBag، چون اگر مثلا ViewBag.Country1 را وارد کنیم، در زمان اجرا تنها چیزی نمایش داده نخواهد شد؛ اما با روش Strongly typed view، حتما خطای Compilation Error به همراه نمایش محل مشکل نهایی، در صفحه ظاهر خواهد شد.
سؤال: آیا میشود پیش از اجرای برنامه هم این بررسی را انجام داد؟
پاسخ: بله. باید فایل پروژه را اندکی ویرایش کرده و مقدار MvcBuildViews را که به صورت پیش فرض false هست، true نمود. یا خارج از ویژوال استودیو با یک ادیتور متنی ساده مثلا فایل csproj را گشوده و این تغییر را انجام دهید. یا داخل ویژوال استودیو، بر روی نام پروژه کلیک راست کرده و سپس گزینه Unload Project را انتخاب کنید. مجددا بر روی این پروژه Unload شده کلیک راست نموده و گزینه edit را انتخاب نمائید. در صفحه باز شده، MvcBuildViews را یافته و آنرا true کنید. سپس پروژه را Reload کنید.
اکنون اگر پروژه را کامپایل کنید، پیغام خطای زیر پیش از اجرای برنامه قابل مشاهده خواهد بود:
'MvcApplication1.Models.Employee' does not contain a definition for 'FirstName1'
and no extension method 'FirstName1' accepting a first argument of type 'MvcApplication1.Models.Employee'
could be found (are you missing a using directive or an assembly reference?)
d:\Prog\MvcApplication1\MvcApplication1\Views\Home\Index.cshtml 10 MvcApplication1
البته بدیهی است این تغییر، زمان Build پروژه را مقداری افزایش خواهد داد؛ اما امنترین حالت ممکن برای جلوگیری از این نوع خطاهای تایپی است.
یا حداقل بهتر است یکبار پیش از ارائه نهایی برنامه این مورد فعال و بررسی شود.
و یک خبر خوب!
مجوز سورس کد ASP.NET MVC از MS-PL به Apache تغییر کرده و همچنین Razor و یک سری موارد دیگر هم سورس باز شدهاند. این تغییرات به این معنا خواهند بود که پروژه از حالت فقط خواندنی MS-PL به حالت متداول یک پروژه سورس باز که شامل دریافت تغییرات و وصلهها از جامعه برنامه نویسها است، تغییر کرده است (^ و ^).
اصل هفتم: Liskove Substitution Principle
"ارث بری باید به صورتی باشد که زیر نوع را بتوان بجای ابر نوع استفاده کرد"
این اصل میگوید اگر قرار است
از ارث بری استفاده شود، نحوهی استفاده باید بدین گونه باشد که اگر یک شیء از کلاس
والد ( Base-Parent-Super type ) داشته باشیم، باید بتوان آن را
با شیء کلاس فرزند ( Sub
Type-Child ) بدون
هیچ گونه تغییری در منطق کد استفاده کننده از شیء مورد نظر، تغییر داد. به زبان
ساده باید بتوان شیء فرزند را جایگزین شیء والد کرد.
نکته مهم: این اصل در مورد عکس این رابطه صحبتی نمیکند و دلیل آن هم منطق طراحی میباشد. تصور کنید که شیء ای داشته باشید که از یک کلاس والد، ارث برده باشد. نوشتن کدی که شیء والد را بتوان جایگزین شیء فرزند کرد، بسیار سخت است؛ چرا که منطق متکی بر کلاس فرزند بسیار وابسته به جزییات کلاس فرزند است. در غیر این صورت وجود شیء فرزند، کم اهمیت میباشد.
با رعایت این اصل، میتوانیم در مواقعی که شروط مرتبط با کلاس فرزند را نداریم و یک سری منطق و قیود کلی مرتبط با کلاس والد را داریم، از شیء کلاس والد استفاده نماییم و وظیفه نمونه گیری (instantiation ) آن را به یک کلاس دیگر محول کنیم. به مثال زیر توجه کنید:
public class Parent { public string Name { get; set; } public int X { get; set; } public int Y { get; set; } public Parent() { X = Y = 0; } public virtual void Move() { X += 5; Y += 5; } public void Shoot() { } public virtual void Pass() { } } public class Child1 : Parent { public override void Move() { X += 10; Y += 10; } } public class Child2 : Parent { public override void Move() { X += 20; Y += 20; } } public enum State { Start, Move, Shoot, Pass } public class Creator { public static Parent GetInstance(bool? condition) { if (condition == null) { return new Parent(); } if (condition == true) { return new Child1(); } else { return new Child2(); } } } public class Context { public void SetState(ref State s) { s = State.Move; } public void Main() { State state =State.Start; // در مورد نوع این شیء چیزی نمیدانیم و وابسته به شرایط نوع آن متغیر است // در حقیقت شیء کلاس فرزند را جای شیء کلاس والد قرار میدهیم و نه بالعکس Parent obj = Creator.GetInstance(null); // منطق برنامه وضعیت را تغییر میدهد SetState(ref state); // قواعد کلی و عمومی که بدون در نظر گرفتن کلاس (نوع) شیء بر آن اعمال میشود switch (state) { case State.Move: obj.Move(); break; case State.Shoot: obj.Shoot(); break; case State.Pass: obj.Pass(); break; default: break; } } }
همانطور که در کدها نیز توضیح دادهام، کلاسهای فرزند را جایگزین کلاس والد کردهایم. اگر میخواستیم عکس رابطه را (شیء والد را به شیء فرزند انتقال دهیم) اعمال کنیم باید تغییر زیر را ایجاد میکردیم که با خطا روبرو خواهد شد:
Child1 obj = Creator.GetInstance(null);
اصل هشتم: Interface segregation
"واسطهای کوچک بهتر از واسطهای حجیم است"
این اصل به ما میگوید در تعریف واسطهای متعدد خساست به خرج ندهیم و بجای آنکه یک واسط اصلی با وظیفههای بسیار داشته باشیم، بهتر است واسطهای متعددی با وظیفههای کمتر داشته باشیم. برای درک این اصل ساده به عقب برمیگردیم، جایی که نیاز به واسط را توضیح دادیم. واسط، نقش تعریف پروتکل را دارد. اگر قرار باشد واسطی بزرگ با چندین مسئولیت داشته باشیم، آنگاه تعریف مستحکمی را از وظیفهی واسط ارائه ندادهایم. لذا هر کلاس پیاده ساز این واسط، برخی وظیفههایی را که نیاز به آن ندارد، باید تعریف و پیاده سازی کند. به مثال زیر نگاه کنید:
public interface IHuman { void Move(); void Eat(); void LevelUp(); void FireBullet(); } public class Player : IHuman { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } } public class Enemy : IHuman { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } } public class Citizen : IHuman { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } }
در این مثال که مربوط به مدل یک بازی با نقشهای بازیکن، دشمن و شهروند (بی گناه!) است، طراحی به گونهای است که دشمن و شهروند، توابعی را که نیاز ندارند، باید پیاده سازی کنند. در دشمن: Eat(), LevelUp() و در شهروند: Eat(), LevelUp(), FireBullet() . لذا واسط IHuman یک واسط کلی با وظیفههای متعدد است.
در مدل بهبود یافته که کلاسها با پسوند Better بازنویسی شدهاند داریم:
public interface IMovable { void Move(); } public interface IEatable { void Eat(); } public interface IPlayer { void LevelUp(); } public interface IShooter { void FireBullet(); } public class PlayerBetter : IPlayer, IMovable, IEatable, IShooter { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } } public class EnemyBetter : IMovable, IShooter { public void FireBullet() { } public void Move() { } } public class CitizenBetter : IMovable { public void Move() { } }
در اینجا برای هر وظیفه یک واسط تعریف کرده ایم که باعث قوی شدن معنای هر واسط میشود.
اصل نهم: Dependency inversion
"وابستگی بین ماژولها را به وابستگی آنها به انتزاع (واسط) تغییر بده"
این اصل که نمود آن را در الگوهای طراحی dependency injection و factory میبینیم، میگوید که ماژولهای بالادست (ماژول استفاده کننده ماژول پایین دست) به جای آنکه ارجاع مستقیمی را به ماژولهای پایین دست داشته باشند، به انتزاعی (واسط) ارجاع بدهند که ماژول پایین دست آنرا پیاده سازی میکند یا به ارث میبرد. در واقع این اصل برای از بین بردن وابستگی قوی بین ماژولهای بالا دست و پایین دست، به میدان آمده است. دو حکم اصلی از این اصل بر میآید:
الف – ماژولهای بالا دست نباید وابسته به ماژولهای پایین دست باشند. هر دو باید وابسته به انتزاع (واسط) باشند. وابستگی ماژول بالا دست از نوع ارجاع و وابستگی ماژول پایین دست از نوع ارث بری است.
ب – انتزاع نباید وابسته به جزییات باشد، بلکه جزییات باید وابسته به انتزاع باشد. یعنی در پیاده سازی منطق برنامه (که جزییات محسوب میشود) باید از واسطها یا کلاسهای انتزاعی استفاده کنیم و همچنین در نوشتن کلاسهای انتزاعی نباید هیچ گونه ارجاعی را به کلاسهای جزیی داشته باشیم.
در مثال زیر با نمونهای از طراحی ناقض این اصل روبرو هستیم:
public class Controller { public Service Service { get; set; } public Controller() { // کنترلر باید نحوه نمونه گیری را بداند (ورودیهای لازم) و این از وظایف آن خارج است Service = new Service(1); } public void DoWork() { Service.RunService(); } } public class Service { public int State { get; set; } public Service(int s) { State = s; } public void RunService() { } }
در این مثال کلاس کنترلر، ماژول بالادست و کلاس سرویس، ماژول پایین دست محسوب میگردد. در ادامه طراحی مطلوب را نیز ارائه دادهام:
public class ControllerBetter { // ارجاع به واسط باعث انعطاف و کاهش وابستگی شده است public IService Service { get; set; } public ControllerBetter(IService service) { // یک کلاس دیگر وظیفه ارسال سرویس به سازنده کلاس کنترلر را دارد // و مسئولیت نمونه گیری را از دوش کنترلر برداشته است Service = service; } public void DoWork() { Service.RunService(); } } // کاهش وابستگی با تعریف واسط و تغییر وابستگی مستقیم بین کنترلر و سرویس public interface IService { void RunService(); } // وابستگی جزییات به انتزاع public class ServiceBetter : IService { public int State { get; set; } public ServiceBetter(int s) { State = s; } public void RunService() { } }
نحوه بهبود طراحی را در توضیحات داخل کد مشاهده میکنید. در مقاله بعدی به اصول GRASP خواهم پرداخت.
React 16x - قسمت 31 - React Hooks - بخش 2 - مقایسه حالتهای مختلف مدیریت حالت با useState Hook
// فقط یک ترد امکان دسترسی به کد را داشته باشد private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1,1); public async Task RunAsync() { // شروع تمام تردها معلق خواهند شد تا این سمافور به پایان برسد // پس از پایان کار ترد جاری، فقط یک ترد در حال انتظار، مجوز دسترسی به قطعه کد بعدی را خواهد یافت و به همین ترتیب برای سایر تردها await _semaphoreSlim.WaitAsync(); try { await Task.Delay(20000); // Your code here } finally { _semaphoreSlim.Release(); } }
EF Code First #14
مهندس عزیز , من در برنامم در یه فرم برا خروج باید entity رو چک کنم که اگه کاربر درخواست درج یه رکورد رو زده باشه و بعدش حداقل یک پارامتر رو پر کرده و دکمه ذخیره رو نزده براش یه پیغام بیاد که شما مایل به درج رکورد هستید یانه.
جطوری میتونم این حالت رو چک کنم؟
برا ویرایش مشکلی نیست , طبق گفته خودتون state رو چک میکنم اگه modifed بود پیغام مایل به ذخیره تغرات رو میدم اما برا درج نمیدونم چه شرطی رو بنویسم