مطالب
دادن «حق فراموش شدن» به کاربران در ASP.NET Core Identity 2.1
برنامه‌هایی که بخواهند سازگار با GDPR باشند، باید اصل 17 ام آن را که حق فراموش شدن است «Right to erasure ('right to be forgotten') »، پیاده سازی کنند؛ به علاوه اشخاص باید بتوانند اطلاعات شخصی خودشان را نیز بدون درنگ از آن سایت دریافت کنند. به عبارتی اگر شخصی در سایت شما ثبت نام کرد، باید قسمتی را هم در آن درنظر بگیرید که کاربر با فشردن یک کلیک، بتواند اکانت فعلی خودش را برای همیشه محو و نابود کند؛ بدون اینکه اثری از آن باقی بماند. همچنین پیش از این عمل نیز باید بتواند با کلیک بر روی یک دکمه، کلیه اطلاعات شخصی خودش را دانلود و ذخیره کند. از زمان ASP.NET Core Identity 2.1، هر دو مورد جزئی از این فریم ورک شده‌اند:



روش پیاده سازی دریافت اطلاعات شخصی در ASP.NET Core Identity 2.1

اگر به IdentityUser نگارش‌های اخیر ASP.NET Core Identity دقت کنید، شامل ویژگی جدید PersonalData نیز هست:
public class IdentityUser<TKey> where TKey : IEquatable<TKey>
{
   [PersonalData]
   public virtual TKey Id { get; set; }
PersonalData یک نشانه‌گذار است و هدف از آن، مشخص کردن خواصی است که شخص می‌تواند درخواست دریافت اطلاعات مرتبط با آن‌ها را بدهد.
سپس زمانیکه در تصویر فوق بر روی دکمه‌ی download کلیک کرد، ابتدا توسط userManager.GetUserAsync، اطلاعات کاربر جاری از بانک اطلاعاتی دریافت شده و سپس حلقه‌ای بر روی تمام خواص شیء IdentityUser تشکیل می‌شود. در اینجا هر کدام که مزین به PersonalDataAttribute بود، مقدارش دریافت شده و به دیکشنری personalData اضافه می‌شود.
var personalData = new Dictionary<string, string>();
var personalDataProps = typeof(TUser).GetProperties().Where(
    prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
foreach (var p in personalDataProps)
{
    personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
}
در آخر این اطلاعات با فرمت JSON، جهت دریافت به کاربر ارائه خواهد شد:
Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json");
return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json");


روش پیاده سازی دادن «حق فراموش شدن» به کاربران

اگر به تصویر ارسال شده دقت کنید، یک دکمه‌ی Delete نیز در آن قرار دارد. کار آن، حذف واقعی و فیزیکی کاربر جاری و تمام اطلاعات وابسته‌ی به او از بانک اطلاعاتی است. این دکمه نیز بدون هیچ واسطی در اختیار خود کاربر قرار گرفته‌است. یعنی به این صورت نیست که ابتدا فرمی را پر کند و سپس شخص دیگری اکانت او را حذف کند.
روش پیاده سازی آن نیز به صورت زیر است:
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
    return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

RequirePassword = await _userManager.HasPasswordAsync(user);
if (RequirePassword)
{
    if (!await _userManager.CheckPasswordAsync(user, Input.Password))
    {
        ModelState.AddModelError(string.Empty, "Password not correct.");
        return Page();
    }
}
برای اینکه کاربر بتواند درخواست حذف اکانت خود را صادر کند، باید ابتدا کلمه‌ی عبور خود را نیز وارد کند. همانطور که مشاهده می‌کنید توسط متد CheckPasswordAsync سرویس userManager، کار صحت کلمه‌ی عبور درخواستی بررسی می‌شود. سپس سطر زیر، اکانت او را حذف می‌کند:
var result = await _userManager.DeleteAsync(user);
موجودیت کاربر با سایر موجودیت‌های دیگری که در ارتباط است، به صورت OnDelete(DeleteBehavior.Cascade) تنظیم شده‌است. یعنی اگر این کاربر حذف شود، تمام اطلاعات او در سایر جداول وابسته نیز به صورت خودکار حذف خواهند شد.
و در آخر، کوکی جاری شخص لاگین شده را نیز حذف می‌کند تا برای همیشه با او خداحافظی کند!
await _signInManager.SignOutAsync();

 

پ.ن.
ما در این سایت هیچگاه شما را فراموش نخواهیم کرد!
نظرات مطالب
معرفی Kendo UI
ممنون
در layout کد زیر هست
 <!-- Bundeling-->
    @BundleConfig.AddStyles("~/bundles/css/",
            "~/content/css/bootstrap.min.css",
            "~/content/css/bootstrap-reset.css",
            "~/content/css/style.css",
            "~/content/css/style-responsive.css"

                            )
    @BundleConfig.AddScripts("~/bundles/js",
                            "~/Scripts/jquery-1.9.1.min.js",
                            "~/Scripts/jquery.validate.min.js",
                            "~/Scripts/jquery.unobtrusive-ajax.min.js",
                            "~/Scripts/jquery.validate.unobtrusive.min.js",
                            "~/Scripts/bootstrap.min.js")
    @RenderSection("JavaScript", required: false)
برای اطمینان از دوبار تعریف شدن جی کوئری خط تعریف فایل‌های جاوااسکریپت رو هم حذف کردم که خطای شناسایی $ رو داد و بعد هم به طور جداگانه جی کوئری رو در همون section تعریف کردم و باز خطای قبلی
نظرات مطالب
ASP.NET MVC #18
- در این تنظیمات فرض بر این است که پوشه content محل قرارگیری فایل‌های css است و همچنین این پوشه در ریشه سایت قرار دارد.
- در MVC نیازی به استفاده از تنظیم authorization -> deny users در وب کانفیگ نیست. روش مرجح استفاده از فیلتر Authorize است که توضیح داده شد.
 
- و بهتر است فایل‌های css و js از سیستم مسیریابی MVC حذف شوند:

// in RegisterRoutes -> Global.asax.cs
routes.IgnoreRoute("{*js}", new { js = @".*\.js(/.*)?"});
routes.IgnoreRoute("{*css}", new { css = @".*\.css(/.*)?"});
مطالب
بررسی چند کتابخانه آپلود با پشتیبانی از DragDrop
برای یکی از پروژه‌ها نیاز به یک آپلودر داشتم که قابلیت  Drag&Drop را نیز داشته باشد و در ضمن پیاده سازی آسانی هم داشته باشد. در این بین به تعدادی از کتابخانه‌های جی کوئری می‌پردازیم.
FileDrop
اولین کتابخانه‌ای که با آن آشنا شدم و از آن استفاده کردم، کتابخانه‌ی FileDrop است که بسیار ساده و در عین حال قابلیت‌های خوبی را می‌دهد و از فناوری Filereader  (+) در Html5 برای اینکار استفاده می‌کند. مرورگرهای کروم، فایرفاکس 3.6 به بعد، IE10 به بعد و Opera 12 به بعد از آن پشتیبانی می‌کنند.
فایل‌های مورد نیاز را از اینجا دانلود کنید . فایل اسکریت آن را ابتدا صدا بزنید:
 <script src="~/scripts/jquery.filedrop.js" type="text/javascript"></script>
سپس المان‌های زیر را به کد HTML خود اضافه کنید:
 <div id="dropZone">فایل برنامه را به داخل این کادر بکشانید</div>
        <br>
        فایل یا فایل‌های آپلود شده:
        <ul id="uploadResult"></ul>
تگ اول، محلی است که فایل‌ها به سمت آن درگ و روی آن دراپ می‌شوند که از این به بعد به آن محل آپلود می‌گوییم. المان بعدی جهت گزارش فایل‌هایی است که آپلود شده‌اند. با آپلود شدن هر تعداد فایل، اسم آن به لیست اضافه می‌گردد.
کدهای css زیر را هم به صفحه اضافه کنید تا محل آپلود زیباتر شود:
        .files {
            min-height: 42px;
            background: #CCC none repeat scroll 0% 0%;
            border-top: 1px solid #FFF;
            margin: 11px 0px;
            padding: 11px 13px;
            border-radius: 6px;
        }

        #dropZone.mouse-over {
            background-color: #1d4257;
        }

کد جی کوئری زیر را به صفحه اضافه کنید:
      $('#dropZone').filedrop({
                url: uploadAddress,
                paramname: 'files',
                maxFiles: 1,
                dragOver: function() {
                    $('#dropZone').addClass('mouse-over');
                },
                dragLeave: function() {
                    $('#dropZone').removeClass('mouse-over');
                },
                drop: function() {
                    $('#dropZone').removeClass('mouse-over');
                },
                afterAll: function() {
                    $('#dropZone').html('آپلود با موفقیت انجام شد');
                },
                uploadFinished: function(i, file, response, time) {
                    $('#uploadResult').append('<li>' + file.name + '</li>');
                }
            });
ابتدا پلاگین جی کوئری را روی تگ مربوطه اعمال می‌کنیم و سپس پارامترها را با مشخصات زیر اعمال می‌کنیم:
Url  آدرسی که قرار است فایل‌ها به آن سمت ارسال شوند. 
 Paramname  در سمت سرور باید فایل‌ها را با استفاده از این نام پارامتر دریافت  کنید.
 maxFiles  تعداد فایلهایی که میتوان با درگ و دراپ کردن روی آن به دست آورد. در بالا به یک فایل محدود شده است.
 dragOver  این رویداد زمانی اجرا خواهد شد که اشاره گر با حالت درگ کرده فایل‌ها را به محل آپلود آورده است.
 dragLeave  موقعی که ماوس از محل آپلود خارج می‌شود
 drop  موقعی که شما فایل‌ها را روی محل  آپلود رها می‌کنید.
 afterAll  بعد از اینکه همه کارها تمام شد اجرا می‌شود.(آخرین رویداد)
 uploadFinished  کار آپلود به پایان رسیده است. در مثال بالا پس از پایان آپلود، نام فایل آپلود شده را به کاربر نشان داده‌ایم.

نحوه‌ی دریافت آن در سمت سرور, در یک اکشن متد به صورت زیر است:

[HttpPost]
        public virtual ActionResult UpdateApp(IEnumerable<HttpPostedFileBase>files)
        {
            foreach (HttpPostedFileBase file in files)
            {
                string filePath = Path.Combine(TempPath, file.FileName);
                file.SaveAs(filePath);
            }

            return Json(new {state = "success", message = "با موفقیت عملیات ارسال فایل انجام شد"}, JsonRequestBehavior.AllowGet);
        }

در اکشن متد بالا ما فایل‌ها را از طریق نام پارامتر files که مشخص کرده بودیم، به عنوان یک لیست شمارشی دریافت می‌کنیم.  کدها بالا برای ساده‌ترین راه اندازی ممکن کفایت می‌کنند.

این موارد از اصلی‌ترین‌ها هستند که به کار می‌آیند. به غیر این‌ها یک سری خصوصیات اضافه‌تری هم برای آن وجود دارد.


fallback_id 
 اگر دوست دارید این آپلودر را نیر به یک آپلودر معمولی اتصال دهید از این شناسه استفاده کنید.
 withCredentials   با استفاده از کوکی‌ها یک درخواست cross-origin ایجاد می‌کند.
 data  اگر دوست دارید به همراه فایل‌ها اطلاعات دیگری هم به همراه آن ارسال و پست شوند از این طریق اقدام نمایید. می‌تواند در قالب یک متغیر باشد یا خروجی یک تابع.
data: {
        param1: 'value1',           
        param2: function(){
            return calculated_data; 
        }
 headers برای ارسال مقدار اضافه‌تر در هدر درخواست به کار میرود و صدا زدن آن همانند کد data می‌باشد. 
 error   در صورتیکه در فرایند آپلود خطایی رخ دهد، اجرا می‌گردد. نحوه‌ی کدنویسی آن و بررسی خطاهای آن به شرح زیر است:
error: function(err, file) {
        switch(err) {
            case 'BrowserNotSupported':
                alert('مرورگر از این فناوری پشتیبانی نمی‌کند')
                break;
            case 'TooManyFiles':
                // قصد آپلود همزمان فایل‌های بیشتری از حد مجاز تعیین شده دارید
                break;
            case 'FileTooLarge':
                //حداقل حجم یکی از فایل‌ها از حجم مجاز تعیین شده بیشتر است
                //برای دسترسی به نام آن فایل از کد زیر استفاده کنید
              //file.name
                break;
            case 'FileTypeNotAllowed':
                // نوع حداقل یکی از فایل‌ها با نوع‌ها مشخص شده ما یکی نیست
                break;
            case 'FileExtensionNotAllowed':
                // پسوند حداقل یکی از فایل‌ها مورد تایید نیست
                break;
            default:
                break;
        }
    }
 allowedfiletypes   نوع فایل‌های مجاز را تعیین می‌کند:
allowedfiletypes: 
['image/jpeg','image/png','image/gif']
 allowedfileextensions   پسوند فایل هایی که برای آپلود مجاز هستند را معرفی می‌کند.
allowedfileextensions:
 ['.apk','.jar']
 maxfilesize   حداکثر حجم مجاز برای هر فایل که به مگابایت بیان می‌شود.
 docOver   این رویداد زمانی اجرا می‌شود که فایل‌های درگ شده شما وارد محیط یا پنجره مرورگر می‌شود.
 uploadStarted 
 این رویداد زمانی اجرا میگردد که فرایند آپلود هر فایل به طور جداگانه در حال آغاز شدن است:
متغیر i در کد زیر شامل اندیس فایلی است که آپلودش آغاز شده است و این اندیس از صفر آغاز می‌شود.
متغیر file دسترسی شما را به اطلاعات یک فایل باز میکند مانند نام فایل.
متغیر len تعداد فایل هایی را که کاربر در محل آپلود رها کرده است، باز میگرداند.
function(i, file, len){

    },
uploadFinished 
با اتمام آپلود هر فایل، این رویداد فراخوانی می‌گردد. دو پارامتر اول آن، همانند سابق هستند. پارامتر response خروجی json ایی را که در سمت سرور برگرداندیم، به ما باز می‌گرداند. پارامتر بعدی، زمانی را که برای آپلود طول کشیده است، بر می‌گرداند.
 function(i, file, response, time) {
    }
progressUpdated 
این رویداد برای نمایش پیشرفت یک آپلود مناسب است که آخرین پارامتر آن یک عدد صحیح از پیشرفت فایل را بر می‌گرداند.
function(i, file, progress) {
    },
globalProgressUpdated 
این رویداد میزان پیشرفت کلیه فایل‌ها را به درصد باز می‌گرداند:
function(progress) {
        $('#progress div')
.width(progress+"%"); }
speedUpdated 
سرعت آپلود هر فایل را با کیلوبیت بر ثانیه مشخص می‌کند.
function(i, file, speed) {
    }
rename
در صورتی که قصد تغییر نام فایل ارسالی را دارید می‌توانید از این رویداد استفاده کنید. پارامتر name، نام اصلی فایل را بر می‌گرداند که می‌توانید آن را دستکاری کنید و نام جدیدی را به عنوان خروجی برگردانید. نمونه کاربردی از این رویداد
 rename: function(name) {
    }

beforeEach 
این رویداد قبل از آپلود هر فایل آغاز می‌گردد و برگرداندن مقدار false در آن باعث جلوگیری و کنسل شدن آپلود آن فایل می‌گردد.
function(file) {
    }

beforeSend 
پارامترهای اولی تکراری هستند ولی آخرین پارامتر یک تابع done را می‌توان به آن پاس کرد که قبل از اجرای کل عملیات آپلود صدا زده می‌شود.
 function(file, i, done) {
    }
رویدادی به اسم queuefiles هم هست تعداد فایل‌هایی را که میتوانند به طور موازی و همزمان آپلود گردند، مشخص می‌کند. ولی دراین حالت maxfiles مورد استفاده قرار نمی‌گیرد. جهت بررسی یک مثال عملی و همچنین کدهای سمت سرور در PHP میتوانید از این آموزش استفاده کنید.
با تستی که به صورت لوکال رو آن انجام دادم به نظر نمی‌رسد برای فایل‌های با حجم متوسط به بالا مناسب باشد و برای فایل‌های با حجم کم مناسب می‌باشد. یک فایل 8 مگابایتی در حالت لوکال 9 ثانیه آپلود آن زمان برد و برای فایل‌های بزرگتر، فایرفاکس دیالوگ Stop Script را نشان داد.

PlUpload
این کتابخانه متن باز هم بسیار کارآمد و ساده و قابل انعطاف است و مثالهای آماده زیادی دارد. سایت سابسن هم در بخش آپلود زیرنویس‌ها از این کتابخانه استفاده می‌کند. از آنجا که آموزش این کتابخانه در سایت جاری آمده است از ذکر نکات بیشتر در مورد آن خودداری می‌نماییم.

Bootstrap FileStyle
اگر از قالب بوت استراپ استفاده می‌کنید و دوست دارید روی المان input file  قدیمی، ولی به شکلی مدرن کار کنید این کتابخانه هم فراموش نشود.

DropZoneJS
این کتابخانه به نسبت DropFile امکانات بیشتری را دارد و در سایت اختصاصی آن مثال‌ها و مستندات خوبی قرار گرفته است. در ساده‌ترین حالت آن ابتدا فایل کتابخانه  را صدا زده و سپس تگ فرم را به آن نسبت دهید:
<script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script>

<form action="/upload-target" class="dropzone"></form>
ولی اگر بخواهید آن را به سمت سرور ارسال کنید و  از آنجا آن را کنترل کنید، کد فرم را به شکل زیر تغییر دهید:
ابتدا بسته‌ی نیوگت آن را صدا بزنید:
Install-Package dropzone

با نصب این کتابخانه یک سری فایل CSS هم به سیستم اضافه می‌شود که می‌توانید برای استایل دهی هر چه بیشتر از آن بهره ببرید. کد فرم را به شکل زیر تغییر دهید:
    <form action="~/Home/SaveUploadedFile" method="post" enctype="multipart/form-data" class="dropzone" id="dropzoneForm" style="width: 50px; background: none; border: none;">
        <div class="fallback">
            <input name="file" type="file" multiple />
            <input type="submit" value="Upload" />
        </div>
    </form>
تگی که با کلاس fallback مزین شده است موقعی به کار می‌آید که مرورگر از این کتابخانه پشتیبانی نکرده و مجبور به نمایش یک آپلود معمولی می‌شویم.
با استفاده از کدنویسی هم می‌توان یک المان را به یک آپلودر تبدیل کرد:
var myDropzone = new Dropzone("div#myId", { url: "/file/post"});

//============ OR ====================
$("div#myId").dropzone({ url: "/file/post" });
همانطور که می‌بینید الزامی برای اینکه از یک تگ فرم استفاده کنید ندارد.
برای کانفیگ آپلودرهایی که از طریف المانهای Html ایجاد می‌شوند، می‌توان از کد زیر استفاده کرد و یک تنظیم عمومی برای تمامی آپلودرهای html آن صفحه ایجاد کرد.
Dropzone.options.myId= {
  paramName: "file", //نام پارامتری که فایل از طریق آن انتقال می‌بابد
  maxFilesize: 2, // MB
  accept: function(file, done) {
    if (file.name == "justinbieber.jpg") {
      done("Naha, you don't.");
    }
    else { done(); }
  }
};
تابع بالا یک آرگومان از نوع file را برگردانده و اگر این تابع، تابع done را با پارامتری رشته‌ای صدا بزند، عملیات آپلود آن فایل کنسل شده و پیام خطایی را نمایش می‌دهد و در صورتیکه بدون پارامتر صدا زده شود، عمل آپلود بدون مشکل انجام می‌شود.
ازآنجا که این کتابخانه از تنظیمات وسیعی استفاده می‌کند و از حوصله‌ی این مقاله خارج است، بهتر هست که صفحه‌ی مستندات آن را که کامل هم هست، مطالعه بفرمایید. از سری قابلیت‌هایی که پشتیبانی می‌کند: موارد پوشش داده شده در FileDrop، ساخت layout، ایجاد صف، متد حذف و اضافه و از این قبیل، ایجاد تصویر تمبر مانند و ...

یک نکته تکمیلی در مورد آپلود: در ASP.net به طور پیش فرض نهایت حجم فایل آپلودی 4 مگابایتی تعیین شده است که میتوانید آن را از طریق web.config تغییر دهید:
<configuration>
    <system.web>
        <httpRuntime maxRequestLength="1048576" />
    </system.web>
</configuration>
برای IIS 7 به بعد هم از تکه کد زیر استفاده کنید:
<system.webServer>
   <security>
      <requestFiltering>
         <requestLimits maxAllowedContentLength="1073741824" />
      </requestFiltering>
   </security>
 </system.webServer>
در هر دو کد بالا نهایت حجم بر روی یک گیگابایت تعیین شده است که maxRequestLength به صورت کیلوبایت و maxAllowContentLength به صورت بایت تعیین شده است. توصیه می‌شود هر دو شکل آن را وارد کنید. به خصوص که IIS Express از کد ابتدایی استفاده می‌کند و بخواهید نتیجه‌ی آن را در تست‌ها ببینید.

نظرات مطالب
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
سلام. من کد‌های زیر رو نوشتم اما دیلیت خطی ( ردیفی) برام کار نمی‌کنه و اررو 500 میده:
$("#JQGrid1").jqGrid({
                url: "/manager/Products/OnProductDataRequested",
                editurl: '/manager/Products/EditProductData',
                mtype: "GET",
                datatype: "json",
                page: 1,
                sortname: 'Priority',
                sortorder: "desc",
                viewrecords: true,
                jsonReader: { id: "Id" },
                prmNames: { id: "Id" },
                colNames: ["Id","خلاصه","توضیح خلاصه","قیمت","قیمت با تخفیف","درصد کارمزد سایت","آستانه هشدار موجودی","محتوا", "عنوان","فروشگاه","گروه","تعداد موجودی", "اولویت","محصولات مکمل","تصاویر","مقادیر مشخصه محصول","قیمت","نظرات", "فعال","ویژه","موجود","برند","عملیات درجا","عملیات کامل"],
                colModel: [
                    { key: true, width: 50, name: "Id", hidden: true, search: false },
                    {
                        editable: true, width: 10, name: "Abstract", search: true, stype: "text", editable: true,
                        hidden: true,
                        editrules: { edithidden: true }
                    },
                    {
                        editable: true, width: 10, name: "AbstractDescription", search: true, stype: "text", editable: true,
                        hidden: true,
                        editrules: { edithidden: true }
                    },
                    {
                        editable: true, width: 10, name: "Value", search: true, stype: "text", editable: true,
                        hidden: true,
                        editrules: { edithidden: true }
                    },
                    {
                        editable: true, width: 10, name: "Discount", search: true, stype: "text", editable: true,
                        hidden: true,
                        editrules: { edithidden: true }
                    },
                    {
                        editable: true, width: 10, name: "SiteWagePercentage", search: true, stype: "text", editable: true,
                        hidden: true,
                        editrules: { edithidden: true }
                    },
                    {
                        editable: true, width: 10, name: "InventoryAlertLimit", search: true, stype: "text", editable: true,
                        hidden: true,
                        editrules: { edithidden: true }
                    },
                    {
                        editable: true, width: 10, name: "Context", search: true, stype: "text", editable: true,
                        hidden: true,
                        editrules: { edithidden: true }
                    },
                    {
                        editable: true, width: 150, name: "Title", search: true, stype: "text",
                        searchoptions: { "sopt": ["bw", "eq"] }
                    },
                   {
                       name: "shopTitle", align: 'center', viewable: true, editrules: { edithidden: true },
                       search: true,
                       editable: true, stype: 'select',
                       edittype: 'select',
                       searchoptions: {
                           sopt: ["eq", "ne"],
                           dataUrl: "/manager/Products/Getshop/", buildSelect: function (data) {
                               var response, s = '<select>', i;
                               response = jQuery.parseJSON(data);
                               if (response && response.length) {
                                   $.each(response, function (i) {
                                       s += '<option value="' + this.shId + '">' + this.shTitle + '</option>';
                                   });
                               }
                               return s + '</select>';
                           }, 
                       },
                       editoptions: {
                           dataUrl: "/manager/Products/Getshop",
                           buildSelect: function (data) {
                               var response, s = '<select>', i;
                               response = jQuery.parseJSON(data);
                               if (response && response.length) {
                                   $.each(response, function (i) {
                                       s += '<option value="' + this.shId + '">' + this.shTitle + '</option>';
                                   });
                               }
                               return s + '</select>';
                           },
                       }
                   },
                    {
                        editable: true, name: "groupTitle", search: true, stype: "select"
                        , editrules: { edithidden: true },
                        search: true,
                        edittype: 'select',
                        editoptions: {
                        dataUrl:  "/manager/Products/GetGroups",
                          buildSelect: function (data) {
                              var response, s = '<select>', i;
                              response = jQuery.parseJSON(data);
                              if (response && response.length) {
                                  $.each(response, function (i) {
                                      s += '<option value="' + this.grpId + '">' + this.grpTitle + '</option>';
                                  });
                              }
                              return s + '</select>';
                          },
                          }
                    },
                    {
                        editable: true, width: 70, name: "AvailableCount", search: true, stype: "number",
                        searchoptions: { "sopt": ["bw", "eq"] }
                    },
                   {
                       editable: true, width: 50, name: "Priority", search: true, stype: "number",
                       searchoptions: { "sopt": ["bw", "eq"] }
                   },
                   {
                       editable: false, width: 80, name: "ComplementProducts", search: true, stype: "text",
                       searchoptions: { "sopt": ["bw", "eq"] }
                   },
                   {
                       editable: false, width: 70, name: "Images", search: true, stype: "text",
                       searchoptions: { "sopt": ["bw", "eq"] }
                   },
                   {
                       editable: false, width: 100, name: "ProductProperty", search: true, stype: "text",
                       searchoptions: { "sopt": ["bw", "eq"] }
                   },
                   {
                       editable: false, width: 80, name: "Price", search: true, stype: "text",
                       searchoptions: { "sopt": ["bw", "eq"] }
                   },
                   {
                       editable: false, width: 80, name: "Comments", search: true, stype: "text",
                       searchoptions: { "sopt": ["bw", "eq"] }
                   },
                   {
                       editable: true, width: 50, name: "Active", search: true, formatter: 'checkbox', edittype: 'checkbox', editoptions: { value: "True:False" }
                       , formatoptions: { disabled: false}
                   },
                    {
                        editable: true, width: 80, name: "AmazingOffer", search: true, formatter: 'checkbox', edittype: 'checkbox', editoptions: { value: "True:False" }
                        , formatoptions: { disabled: false}
                    },
                    {
                        editable: true, width: 80, name: "Available", search: true, formatter: 'checkbox', edittype: 'checkbox', editoptions: { value: "True:False" },
                        searchoptions: { "sopt": ["bw", "eq"] }, formatoptions: { disabled: false }
                    },
                   
                       {
                           name: 'BrandId', align: 'center', hidden: true, viewable: true, editrules: { edithidden: true },
                           editable: true, stype: 'select',
                           edittype: 'select',
                           editoptions: {
                               dataUrl: "/manager/Products/GetBrands",
                               buildSelect: function (data) {
                                   var response, s = '<select>', i;
                                   response = jQuery.parseJSON(data);
                                   if (response && response.length) {
                                       $.each(response, function (i) {
                                           s += '<option value="' + this.brandId + '">' + this.brandTitle + '</option>';
                                       });
                                   }
                                   return s + '</select>';
                               },
                           }

                       },
                    {
                        name: "myac", width: 80, fixed: true, sortable: false, resize: false, formatter: 'actions',
                        formatoptions: { keys: true }
                       },
                    {
                        editable: false, width: 70, name: "FullEdit", search: true, stype: "text",
                        searchoptions: { "sopt": ["bw", "eq"] }
                    },
                    // BLAH, BLAH, BLAH
                ],
                gridComplete: function () {
                    var ids = jQuery("#JQGrid1").jqGrid('getDataIDs');
                    for (var i = 0; i < ids.length; i++) {
                        var cl = ids[i];
                        ComplementProducts = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/ComplementProducts/Index/" + cl + " }) + '><span class=\"fa fa-shopping-cart\" style='color: white;'></span></a></div>";
                        Images = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/Images/Index/" + cl + " }) + '><span class=\"fa fa-picture-o\" style='color: white;'></span></a></div>";
                        ProductProperty = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/ProductPropertyItems/Index/" + cl + " }) + '><span class=\"fa fa-braille\" style='color: white;'></span></a></div>";
                        Price = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/pricesppitems/Index/" + cl + " }) + '><span class=\"fa fa-line-chart\" style='color: white;'></span></a></div>";
                        Comments = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/Products/Comments/" + cl + " }) + '><span class=\"fa fa-comments\" style='color: white;'></span></a></div>";
                        FullEdit = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/Products/Edit/" + cl + " }) + '><span class=\"fa fa-edit\" style='color: white;'></span></a><a class='btn btn-primary' href=/manager/Products/Details/" + cl + " }) + '><span class=\"fa fa-exclamation-circle\" style='color: white;'></span></a></div>";

                        jQuery("#JQGrid1").jqGrid('setRowData', ids[i], { ComplementProducts: ComplementProducts, Images: Images, ProductProperty: ProductProperty, Price: Price, Comments: Comments, FullEdit: FullEdit});
                    }
                }, loadComplete: function () {

                    var activeButton = getColumnIndexByName('Active');
                    var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); //id's
                    $("tbody > tr.jqgrow > td:nth-child(" + (activeButton + 1) + ") > input",
                      this).click(function (e) {
                          var rowId = $(e.target).closest("tr").attr("id");
                          // alert("active clicked " + rowId);
                          $.ajax({
                              type: "POST",
                              url: "/manager/Products/Activate",
                              data: {
                                  id: rowId
                              },
                              dataType: "json"
                          });

                      });



                    var amazingOfferButton = getColumnIndexByName('AmazingOffer');
                    var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); //id's
                    $("tbody > tr.jqgrow > td:nth-child(" + (amazingOfferButton + 1) + ") > input",
                      this).click(function (e) {
                          var rowId = $(e.target).closest("tr").attr("id");
                          $.ajax({
                              type: "POST",
                              url: "/manager/Products/ShowInAmazingOffer",
                              data: {
                                  id: rowId
                              },
                              dataType: "json"
                          });
                          $('#JQGrid1').trigger('reloadGrid');
                      });

                    var availableButton = getColumnIndexByName('Available');
                    var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); //id's
                    $("tbody > tr.jqgrow > td:nth-child(" + (availableButton + 1) + ") > input",
                      this).click(function (e) {
                          var rowId = $(e.target).closest("tr").attr("id");
                          $.ajax({
                              type: "POST",
                              url: "/manager/Products/Available",
                              data: {
                                  id: rowId
                              },
                              dataType: "json"
                          });
                          $('#JQGrid1').trigger('reloadGrid');
                      });
                },
                height: "auto",
                caption: "",
                viewrecords: true,
                rowNum: 20,
                direction: "rtl",
                pager: jQuery('#JQGrid1_pager'),
                rowList: [10, 20, 30, 40],
                toppager: true,
                jsonReader:
                {
                    root: "rows",
                    page: "page",
                    total: "total",
                    records: "records",
                    repeatitems: false,
                    Id: "0"
                },

            }).jqGrid('navGrid', '#JQGrid1_pager',
                // the buttons to appear on the toolbar of the grid
                { edit: true, add: true, del: true, search: true, refresh: true, view: false, position: "left", cloneToTop: true,searchtext:"جستجو" },
                   // options for the Edit Dialog
                   {
                       width: 450,
                       editCaption: "ویرایش محصول",
                       recreateForm: true,
                       closeAfterEdit: true,
                       viewPagerButtons: false,
                       //afterShowForm: populateGroups,
                       errorTextFormat: function (data) {
                           return 'Error: ' + data.responseText
                       }
                },
                   // Add options
                   { url: '/TabMaster/Create', closeAfterAdd: true },
                   // Delete options
                   { url: '/manager/Products/Remove' },
                   {
                       zIndex: 100,
                       caption: "جستجوی محصول",
                       sopt: ['cn']
                   }
                   );

 
مطالب
نمایش ساختارهای درختی در Blazor
یکی از نکات جالب رندر کامپوننت‌ها در Blazor، امکان فراخوانی بازگشتی آن‌ها است؛ یعنی یک کامپوننت می‌تواند خودش را نیز فراخوانی کند. از همین قابلیت می‌توان جهت نمایش ساختارهای درختی، مانند مدل‌های خود ارجاع دهنده‌ی EF استفاده کرد.


مدل برنامه، جهت تامین داده‌های خود ارجاع دهنده و درختی

فرض کنید قصد داریم لیستی از کامنت‌های تو در تو را مدل سازی کنیم که در آن هر کامنت، می‌تواند چندین کامنت تا بی‌نهایت سطح تو در تو را داشته باشد:
namespace BlazorTreeView.ViewModels;

public class Comment
{
    public IList<Comment> Comments = new List<Comment>();
    public string? Text { set; get; }
}
برای نمونه بر اساس این مدل، منبع داده‌ی فرضی زیر را تهیه می‌کنیم:
using BlazorTreeView.ViewModels;

namespace BlazorTreeView.Pages;

public partial class TreeView
{
    private IReadOnlyDictionary<string, object> ChildrenHtmlAttributes { get; } =
        new Dictionary<string, object>(StringComparer.Ordinal)
        {
            { "style", "list-style: none;" },
        };

    private IList<Comment> Comments { get; } =
        new List<Comment>
        {
            new()
            {
                Text = "پاسخ یک",
            },
            new()
            {
                Text = "پاسخ دو",
                Comments =
                    new List<Comment>
                    {
                        new()
                        {
                            Text = "پاسخ اول به پاسخ دو",
                            Comments =
                                new List<Comment>
                                {
                                    new()
                                    {
                                        Text = "پاسخی به پاسخ اول پاسخ دو",
                                    },
                                },
                        },
                        new()
                        {
                            Text = "پاسخ دوم به پاسخ دو",
                        },
                    },
            },
            new()
            {
                Text = "پاسخ سوم",
            },
        };
}
این قطعه کد partial class که مربوط به فایل TreeView.razor.cs برنامه‌است، در حقیقت کدهای پشت صحنه‌ی کامپوننت مثال TreeView.razor است که در ادامه آن‌را توسعه خواهیم داد. در نهایت قرار است بتوانیم آن‌را به صورت زیر رندر کنیم:



طراحی کامپوننت DntTreeView

برای اینکه بتوانیم به یک کامپوننت با قابلیت استفاده‌ی مجدد بررسیم، کدهای نمایش اطلاعات تو در تو و درختی را توسط کامپوننت سفارشی DntTreeView پیاده سازی خواهیم کرد. پیشنیازهای آن نیز به صورت زیر است:
- این کامپوننت باید جنریک باشد؛ یعنی باید به صورت زیر شروع شود:
/// <summary>
///   A custom DntTreeView
/// </summary>
public partial class DntTreeView<TRecord>
{
چون باید بتوان یک لیست جنریک <IEnumerable<TRecord را به آن، جهت رندر ارسال کرد و قرار نیست این کامپوننت، تنها به شیء سفارشی Comment مثال جاری ما وابسته باشد. بنابراین اولین خاصیت آن، شیء جنریک Items است که لیست کامنت‌ها/عناصر را دریافت می‌کند:
    /// <summary>
    ///     The treeview's self-referencing items
    /// </summary>
    [Parameter]
    public IEnumerable<TRecord>? Items { set; get; }
- هنگام رندر هر آیتم کامنت باید بتوان یک قالب سفارشی را از کاربر دریافت کرد. نمی‌خواهیم صرفا برای مثال Text شیء Comment فوق را به صورت متنی و ساده نمایش دهیم. می‌خواهیم در حین رندر، کل شیء TRecord جاری را به مصرف کننده ارسال و یک قالب سفارشی را از آن دریافت کنیم. یعنی باید یک RenderFragment جنریک را به صورت زیر نیز داشته باشیم تا مصرف کننده بتواند TRecord در حال رندر را دریافت و قالب Htmlای خودش را بازگشت دهد:
    /// <summary>
    ///     The treeview item's template
    /// </summary>
    [Parameter]
    public RenderFragment<TRecord>? ItemTemplate { set; get; }
- همچنین همیشه باید به فکر عدم وجود اطلاعاتی برای نمایش نیز بود. به همین جهت بهتر است قالب دیگری را نیز از مصرف کننده برای اینکار درخواست کنیم و نحوه‌ی رندر سفارشی این قسمت را نیز به مصرف کننده واگذار کنیم:
    /// <summary>
    ///     The content displayed if the list is empty
    /// </summary>
    [Parameter]
    public RenderFragment? EmptyContentTemplate { set; get; }
- زمانیکه با شیء از پیش تعریف شده‌ی Comment این مثال کار می‌کنیم، کاملا مشخص است که خاصیت Comments آن تو در تو است:
public class Comment
{
    public IList<Comment> Comments = new List<Comment>();
    public string? Text { set; get; }
}
اما زمانیکه با یک کامپوننت جنریک کار می‌کنیم، نیاز است از مصرف کننده، نام این خاصیت تو در تو را به نحو واضحی دریافت کنیم؛ به صورت زیر:
    /// <summary>
    ///     The property which returns the children items
    /// </summary>
    [Parameter]
    public Expression<Func<TRecord, IEnumerable<TRecord>>>? ChildrenSelector { set; get; }
دلیل استفاده از Expression Funcها را در مطلب «static reflection» می‌توانید مطالعه کنید. زمانیکه قرار است از کامپوننت DntTreeView استفاده کنیم، ابتدا نوع جنریک آن‌را مشخص می‌کنیم، سپس لیست اشیاء ارسالی به آن‌را و در ادامه با استفاده از ChildrenSelector به صورت زیر، مشخص می‌کنیم که خاصیت Comments است که به همراه Children می‌باشد و تو در تو است:
        <DntTreeView
            TRecord="Comment"
            Items="Comments"
            ChildrenSelector="m => m.Comments"
و مرسوم است جهت بالابردن کارآیی Expression Funcها، آن‌ها را کامپایل و کش کنیم که نمونه‌ای از روش آن‌را به صورت زیر مشاهده می‌کنید:
public partial class DntTreeView<TRecord>
{
    private Expression? _lastCompiledExpression;
    internal Func<TRecord, IEnumerable<TRecord>>? CompiledChildrenSelector { private set; get; }

    // ...

    protected override void OnParametersSet()
    {
        if (_lastCompiledExpression != ChildrenSelector)
        {
            CompiledChildrenSelector = ChildrenSelector?.Compile();
            _lastCompiledExpression = ChildrenSelector;
        }
    }
}
تا اینجا ساختار کدهای پشت صحنه‌ی DntTreeView.razor.cs مشخص شد. اکنون UI این کامپوننت را به صورت زیر تکمیل می‌کنیم:
@namespace BlazorTreeView.Pages.Components
@typeparam TRecord

@if (Items is null || !Items.Any())
{
    @EmptyContentTemplate
}
else
{
    <CascadingValue Value="this">
        <ul @attributes="AdditionalAttributes">
            @foreach (var item in Items)
            {
                <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/>
            }
        </ul>
    </CascadingValue>
}
در ابتدای کار، اگر آیتمی برای نمایش وجود نداشته باشد، EmptyContentTemplate دریافتی از استفاده کننده را رندر می‌کنیم. در غیراینصورت، حلقه‌ای را بر روی لیست Items ایجاد کرده و آن‌‌ها را یکی نمایش می‌دهیم. این نمایش، نکات زیر را به همراه دارد:
- نمایش توسط کامپوننت دومی به نام DntTreeViewChildrenItem انجام می‌شود که آن‌‌هم جنریک است و شیء item جاری را توسط خاصیت ParentItem دریافت می‌کند.
- در اینجا یک CascadingValue اشاره کننده به شیء this را هم مشاهده می‌کنید. این روش، یکی از روش‌های اجازه دادن دسترسی به خواص و امکانات یک کامپوننت والد، در کامپوننت‌های فرزند است که در ادامه از آن استفاده خواهیم کرد.


تکمیل کامپوننت بازگشتی DntTreeViewChildrenItem.razor

اگر به حلقه‌ی foreach (var item in Items) در کامپوننت DntTreeView.razor دقت کنید، یک سطح را بیشتر پوشش نمی‌دهد؛ اما کامنت‌های ما چندسطحی و تو در تو هستند و عمق آن‌ها هم مشخص نیست. به همین جهت نیاز است به نحوی بتوان یک طراحی recursive و بازگشتی را در کامپوننت‌های Blazor داشت که خوشبختانه این مورد پیش‌بینی شده‌است و هر کامپوننت Blazor، می‌تواند خودش را نیز فراخوانی کند:
@namespace BlazorTreeView.Pages.Components
@typeparam TRecord

<li @attributes="@SafeOwnerTreeView.ChildrenHtmlAttributes" @key="ParentItem?.GetHashCode()">
    @if (SafeOwnerTreeView.ItemTemplate is not null && ParentItem is not null)
    {
        @SafeOwnerTreeView.ItemTemplate(ParentItem)
    }
    @if (Children is not null)
    {
        <ul>
            @foreach (var item in Children)
            {
                <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/>
            }
        </ul>
    }
</li>
این‌ها کدهای DntTreeViewChildrenItem.razor هستند که در آن، ابتدا ItemTemplate دریافتی از والد یا همان DntTreeView.razor رندر می‌شود. سپس به کمک CompiledChildrenSelector ای که عنوان شد، یک شیء Children را تشکیل داده و آن‌را به خودش (فراخوانی مجدد DntTreeViewChildrenItem در اینجا)، ارسال می‌کند. این فراخوانی بازگشتی، سبب رندر تمام سطوح تو در توی شیء جاری می‌شود.

کدهای پشت صحنه‌ی این کامپوننت یعنی فایل DntTreeViewChildrenItem.razor.cs به صورت زیر است:
/// <summary>
///     A custom DntTreeView
/// </summary>
public partial class DntTreeViewChildrenItem<TRecord>
{
    /// <summary>
    ///     Defines the owner of this component.
    /// </summary>
    [CascadingParameter]
    public DntTreeView<TRecord>? OwnerTreeView { get; set; }

    private DntTreeView<TRecord> SafeOwnerTreeView =>
        OwnerTreeView ??
        throw new InvalidOperationException("`DntTreeViewChildrenItem` should be placed inside of a `DntTreeView`.");

    /// <summary>
    ///     Nested parent item to display
    /// </summary>
    [Parameter]
    public TRecord? ParentItem { set; get; }

    private IEnumerable<TRecord>? Children =>
        ParentItem is null || SafeOwnerTreeView.CompiledChildrenSelector is null
            ? null
            : SafeOwnerTreeView.CompiledChildrenSelector(ParentItem);
}
با استفاده از یک پارامتر از نوع CascadingParameter، می‌توان به اطلاعات شیء CascadingValue ای که در کامپوننت والد DntTreeView.razor قرا دادیم، دسترسی پیدا کنیم. سپس یکبار هم بررسی می‌کنیم که آیا نال هست یا خیر. یعنی قرار نیست که این کامپوننت فرزند، درجائی به صورت مستقیم استفاده شود. فقط قرار است داخل کامپوننت والد فراخوانی شود. به همین جهت اگر این CascadingParameter نال بود، یعنی این کامپوننت فرزند، به اشتباه فراخوانی شده و با صدور استثنائی این مساله را گوشزد می‌کنیم. اکنون که به SafeOwnerTreeView یا همان نمونه‌ای از شیء والد دسترسی پیدا کردیم، می‌توانیم پارامتر CompiledChildrenSelector آن‌را نیز فراخوانی کرده و توسط آن، به شیء تو در توی جدیدی در صورت وجود، جهت رندر بازگشتی آن رسید.
یعنی این کامپوننت ابتدا ParentItem، یا اولین سطح ممکن و در دسترس را رندر می‌کند. سپس با استفاده از Expression Func مهیای در کامپوننت والد، شیء فرزند را در صورت وجود یافته و سپس به صورت بازگشتی آن‌را با فراخوانی مجدد خودش ، رندر می‌کند.


روش استفاده از کامپوننت DntTreeView

اکنون که کار توسعه‌ی کامپوننت جنریک DntTreeView پایان یافت، روش استفاده‌ی از آن به صورت زیر است:
<div class="card" dir="rtl">
    <div class="card-header">
        DntTreeView
    </div>
    <div class="card-body">
        <DntTreeView
            TRecord="Comment"
            Items="Comments"
            ChildrenSelector="m => m.Comments"
            style="list-style: none;"
            ChildrenHtmlAttributes="ChildrenHtmlAttributes">
            <ItemTemplate Context="record">
                <div class="card mb-1">
                    <div class="card-body">
                        <span>@record.Text</span>
                    </div>
                </div>
            </ItemTemplate>
            <EmptyContentTemplate>
                <div class="alert alert-warning">
                    There is no item to display!
                </div>
            </EmptyContentTemplate>
        </DntTreeView>
    </div>
</div>
همانطور که مشاهده می‌کنید، چون کامپوننت جنریک است، باید نوع TRecord را که در مثال ما، شیء Comment است، مشخص کرد. سپس لیست نظرات، خاصیت تو در تو، قالب سفارشی نمایش Text نظرات (با توجه به Context دریافتی که امکان دسترسی به شیء جاری در حال رندر را میسر می‌کند) و همچنین قالب سفارشی نبود اطلاعاتی برای نمایش را تعریف می‌کنیم.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorTreeView.zip
کامپوننت توسعه یافته‌ی در اینجا در هر دو حالت Blazor WASM و Blazor Server کار می‌کند.
نظرات مطالب
ASP.NET MVC #11
با سلام؛ چند روز پیش متوجه وجود یک مشکل در فرم هایی که مقادیرشون به وسیله Ajax به سرور ارسال می‌شوند توی IE در حالت Compatibility View شدم.

شرح مسئله :
View من به شکل زیره :
    <% using (Html.BeginForm("Forget", "Account", FormMethod.Post, new { encType = "multipart/form-data", id = "forgetForm", name = "forgetForm" }))
                   { %>
                <%: Html.AntiForgeryToken() %>
                <%: Html.ValidationSummary(true) %>

                                    <input id="User_Name" name="User_Name" type="text" data-original-title="لطفاً نام کاربری انتخابی خود را وارد کنید" data-toggle="tooltip" data-placement="top">
                                    <button id="submitForgetForm" type="button"><i></i>  بازیابی کلمه عبور   </button>
                <% } %>
اما مشکل اینجاست که این صفحه توی مرورگر موزیلا و کروم به خوبی کار می‌کنه اما توی اینترنت اکسپلورر در حالت Compatibility View وقتی تابع ()Forget اجرا میشه ، مقدار itm.User_Name نال میشه.(تصویر زیر)


کجای کار اشتباه است ؟ با تشکر
نظرات مطالب
UrlRewriter توسط Intelligencia.UrlRewriter
خواهش میکنم
 شما باید بر اساس کلید در صفحه‌ی مقصد محصولی که فرستادیدو پیدا کنید منظورم کار این خطه 
Label1.Text =
                GetProducts.Get()
                    .Where(x => x.Id == int.Parse(Request.QueryString["PID"].ToString()))
                    .Select(x => x.Name)
                    .FirstOrDefault();
اگه کد محصولو به صفحه‌ی که میخواهید جزییاتو تو اون نشون بدید نفرستید چجوری میخواید محصول مورد نظر رو شناسایی کنید تا بتونید جزییات اونو نشون بدید ؟
البته راه‌های دیگه هم هست که مثلا نام محصولتون به عنوان کلید باشه و بر اساس اون محصولو تو صفحه‌ی مقصد شناسایی کنید که به نظر من اصلا کار منطقی نیست.
یا کد محصولو توسط راه دیگری بجز URL بفرستید مثل session  که غیر منطقی تره .
در کل میتونید  فقط نام محصولو بفرستید که اینطوری میشه
<a href='<%# "Details/" + Eval("Name")%>'><%# Eval("Name") %></a>
ولی چجوری محصولو تو صفحه‌ی مقصد میخواید شناسایی کنید تا جزئیاتشو نشون بدید؟
بهترین راه اینه که کد محصول هم بفرستید.
نظرات مطالب
ASP.NET MVC #12
- یک سری پیشنیاز طراحی رو باید بدونید مانند «کار با کلیدهای اصلی و خارجی در EF Code first ». در اینجا با نحوه تعریف صحیح کلید خارجی به صورت یک خاصیت عددی آشنا خواهید شد. این مورد همان چیزی است که باید از یک drop down list دریافت شود. فقط primary key یک رکورد مهم است نه تمام خواص و فیلدهای مرتبط با آن.
- مرحله بعد ارسال اطلاعات به View هست به این ترتیب:
public ActionResult Index()
{
    var query = db.Users.Select(c => new SelectListItem
                                              {
                                                  Value = c.UserId, 
                                                  Text = c.UserName,
                                                  Selected = c.UserId.Equals(3)
                                              });
    var model = new MyFormViewModel
                    {
                        List = query.ToList()
                    };
    return View(model);
}
در اینجا بر اساس اطلاعات بانک اطلاعاتی SelectListItem‌ها تشکیل شده و به ViewModel فرم جاری ارسال می‌شود (به علاوه باید با ViewModel کار کنید نه مدل جداول بانک اطلاعاتی).
public class MyFormViewModel
{
    public int UserId { get; set; }
    public IList<SelectListItem> List { get; set; }
}
در مورد نگاشت‌ها هم مباحث auto-mapper در سایت هست.
همچنین خاصیت Selected هم در اینجا بر اساس یک شرط تعیین شده.
- قسمت View برنامه هم به این شکل خواهد بود:
@model MyFormViewModel

@Html.DropDownListFor(m => m.UserId, Model.List, "--Select One--")

مطالب
SQL Server CE و ثبت متون طولانی در EF Code first
زمانیکه در EF Code first تعریف خاصیتی به نحو زیر باشد
public string Content { get; set; }
در حین کار با SQL Server به صورت خودکار به nvarchar max نگاشت می‌شود. اما همین تعریف در SQL Server CE به nvarchar 4000 نگاشت خواهد شد؛ چون این بانک اطلاعاتی نوع‌های max دار را پشتیبانی نمی‌کند.
بنابراین اگر هدف، ثبت اطلاعات در فیلدی از نوع ntext در این بانک اطلاعاتی باشد باید به یکی از دو روش زیر عمل کرد:
[MaxLength]
public string Content { get; set; }
بله. فقط کافی است یک MaxLength را بالای خاصیت قرار داد (بدون تعیین طول آن) تا به صورت خودکار در SQL Server CE به ntext نگاشت شود و یا می‌توان نوع ستون را صریحا تعیین کرد:
[Column(TypeName = "ntext")]
public string Text { get; set; }
روش اول بهتر است از این جهت که با بانک‌های اطلاعاتی مختلف سازگاری بهتری دارد. برای مثال نوع ntext در SQL Server کامل، منسوخ شده درنظر گرفته می‌شود اما اگر از ویژگی MaxLength در اینجا استفاده گردد به صورت خودکار به nvarchar max نگاشت خواهد شد و در SQL Server CE به ntext .
بنابراین قید MaxLength بر روی خواصی که قرار است حاوی متونی طولانی باشند، می‌تواند به عنوان یک کار مفید جهت سازگاری با بانک‌های مختلف، به شمار آید.