نظرات مطالب
React 16x - قسمت 19 - کار با فرم‌ها - بخش 2 - اعتبارسنجی ورودی‌های کاربران
validateProperty یک خاصیت را تعیین اعتبار می‌کند، اما برای تطابق پسوردها، نیاز به تعیین اعتبار دو خاصیت را با هم دارد. به همین جهت برای مثال می‌توان پارامتر چهارمی را به نام correspond، در اینجا اضافه کرد تا فیلد متناظر با آن‌را هم مشخص کند:
  renderInput(name, label, type = "text", correspond) {
    const { data, errors } = this.state;
    return (
      <Input
        name={name}
        type={type}
        label={label}
        value={data[name]}
        onChange={this.handleChange}
        error={errors[name]}
        correspond={correspond}
      />
    );
  }
بعد برای نمونه، تعریف فرم ثبت نام، به صورت زیر تغییر می‌کند که در آن confirmPassword هم اضافه شده و فیلد متناظر با آن، password است:
        <form onSubmit={this.handleSubmit}>
          {this.renderInput("username", "Username")}
          {this.renderInput("password", "Password", "password")}
          {this.renderInput(
            "confirmPassword",
            "Confirm Password",
            "password",
            "password"
          )}
          {this.renderInput("name", "Name")}
          {this.renderButton("Register")}
        </form>
در این حالت، داده‌های فرم و اعتبارسنجی‌های آن، به صورت زیر تعریف می‌شوند:
  state = {
    data: { username: "", password: "", name: "", confirmPassword: "" },
    errors: {},
  };

  schema = {
    username: Joi.string()
      .required()
      .email({ minDomainSegments: 2, tlds: { allow: ["com", "net", "info"] } })
      .label("Username"),
    password: Joi.string().required().min(5).label("Password"),
    name: Joi.string().required().label("Name"),
    confirmPassword: Joi.any().valid(Joi.ref("password")).required().messages({
      "any.only": "با رمز عبور مطابقت ندارد",
    }),
  };
و در آخر متد validateProperty بر اساس attribute سفارشی اضافه شده به نام correspond، در صورت وجود و تعریف آن، یک خاصیت پویا را به شیء داده‌های ورودی و همچنین شیء schema اضافه می‌کند که این‌ها سبب خواهند شد تا اینبار اعتبارسنجی بر روی دو فیلد صورت گیرد:
  validateProperty = ({ name, value, attributes }) => {
    const userInputObject = { [name]: value };
    const schemaMap = { [name]: this.schema[name] };

    if (attributes.correspond) {
      const correspondFieldName = attributes.correspond.value;
      userInputObject[correspondFieldName] = this.state.data[
        correspondFieldName
      ];
      schemaMap[correspondFieldName] = this.schema[correspondFieldName];
    }

    const propertySchema = Joi.object(schemaMap);
    const { error } = propertySchema.validate(userInputObject, {
      abortEarly: true,
    });
    return error ? error.details[0].message : null;
  };
مطالب
تبدیل پلاگین‌های jQuery‌ به کنترل‌های ASP.Net

امروز داشتم یک سری از پلاگین‌های jQuery را مرور می‌کردم، مورد زیر به نظرم واقعا حرفه‌ای اومد و کمبود آن هم در بین کنترل‌های استاندارد ASP.Net محسوس است:
Masked Input Plugin
استفاده از آن به صورت معمولی بسیار ساده است. فقط کافی است اسکریپت‌های jQuery و سپس این افزونه به هدر صفحه اضافه شوند و بعد هم مطابق صفحه usage آن عمل کرد.
خیلی هم عالی! ولی این شیوه‌ی متداول کار در ASP.Net نیست. آیا بهتر نیست این مجموعه را تبدیل به یک کنترل کنیم و از این پس به سادگی با استفاده از Toolbox ویژوال استودیو آن‌را به صفحات اضافه کرده و بدون درگیر شدن با دستکاری سورس html صفحه، از آن استفاده کنیم؟ به‌عبارتی دیگر یکبار باید با جزئیات درگیر شد، آنرا بسته بندی کرد و سپس بارها از آن استفاده نمود. (مفاهیم شیءگرایی)

برای این‌کار، یک پروژه جدید ایجاد ASP.Net server control را آغاز نمائید (به نام MaskedEditCtrl).



به صورت پیش فرض یک قالب استاندارد ایجاد خواهد شد که کمی نیاز به اصلاح دارد. نام کلاس را به MaskedEdit تغییر خواهیم داد و همچنین در قسمت ToolboxData نیز نام کنترل را به MaskedEdit ویرایش می‌کنیم.
برای اینکه مجبور نشویم یک کنترل کاملا جدید را از صفر ایجاد کنیم، خواص و توانایی‌های اصلی این کنترل را از TextBox استاندارد به ارث خواهیم برد. بنابراین تا اینجای کار داریم:
namespace MaskedEditCtrl
{
[DefaultProperty("MaskFormula")]
[ToolboxData("<{0}:MaskedEdit runat=server></{0}:MaskedEdit>")]
[Description("MaskedEdit Control")]
public class MaskedEdit : TextBox

{

سپس باید رویداد OnPreRender را تحریف (override) کرده و در آن همان اعمالی را که هنگام افزودن اسکریپت‌ها به صورت دستی انجام می‌دادیم با برنامه نویسی پیاده سازی کنیم. چند نکته ریز در اینجا وجود دارد که در ادامه به آن‌ها اشاره خواهد شد.
از ASP.Net 2.0 به بعد، امکان قرار دادن فایل‌های اسکریپت و یا تصاویر همراه یک کنترل، درون فایل dll آن بدون نیاز به توزیع مجزای آنها به صورت WebResource مهیا شده است. برای این منظور اسکریپت‌های jQuery و افزونه mask edit را به پروژه اضافه نمائید. سپس به قسمت خواص آنها (هر دو اسکریپت) مراجعه کرده و build action آنها را به Embedded Resource تغییر دهید (شکل زیر):



از این پس با کامپایل پروژه، این فایل‌ها به صورت resource به dll ما اضافه خواهند شد. برای تست این مورد می‌توان به برنامه reflector مراجعه کرد (تصویر زیر):



پس از افزودن مقدماتی اسکریپت‌ها و تعریف آنها به صورت resource ، باید آنها را در فایل AssemblyInfo.cs پروژه نیز تعریف کرد (به صورت زیر).

[assembly: WebResource("MaskedEditCtrl.jquery.min.js", "text/javascript")]
[assembly: WebResource("MaskedEditCtrl.jquery.maskedinput-1.1.4.pack.js", "text/javascript")]

نکته مهم: همانطور که ملاحظه می‌کنید نام فضای نام پروژه (namespace) باید به ابتدای اسکریپت‌های معرفی شده اضافه شود.

پس از آن نوبت به افزودن این اسکریپت‌ها به صورت خودکار در هنگام نمایش کنترل است. برای این منظور داریم:
        protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);

//adding .js files
if (!Page.ClientScript.IsClientScriptIncludeRegistered("jquery_base"))
{
string scriptUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"MaskedEditCtrl.jquery.min.js");
Page.ClientScript.RegisterClientScriptInclude("jquery_base", scriptUrl);
}

if (!Page.ClientScript.IsClientScriptIncludeRegistered("edit_ctrl"))
{
string scriptUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"MaskedEditCtrl.jquery.maskedinput-1.1.4.pack.js");
Page.ClientScript.RegisterClientScriptInclude("edit_ctrl", scriptUrl);
}

if (!Page.ClientScript.IsStartupScriptRegistered("MaskStartup" + this.ID))
{
// Form the script to be registered at client side.
StringBuilder sbStartupScript = new StringBuilder();
sbStartupScript.AppendLine("jQuery(function($){");
sbStartupScript.AppendLine("$(\"#" + this.ClientID + "\").mask(\"" + MaskFormula + "\");");
sbStartupScript.AppendLine("});");
Page.ClientScript.RegisterStartupScript(typeof(Page),
"MaskStartup" + this.ID, sbStartupScript.ToString(), true);
}

}

همانطور که ملاحظه می‌کنید، ابتدا WebResource دریافت شده و سپس به صفحه اضافه می‌شود. در آخر مطابق راهنمای افزونه mask edit عمل شد. یعنی اسکریپت مورد نظر را ساخته و به صفحه اضافه کردیم.

نکته جاوا اسکریپتی: علت استفاده از this.ClientID جهت معرفی نام کنترل جاری این است که هنگامیکه کنترل توسط یک master page رندر شود، ID آن توسط موتور ASP.Net کمی تغییر خواهد کرد. برای مثال myTextBox‌ به ctl00_ContentPlaceHolder1_myTextBox تبدیل خواهد شد و اگر صرفا this.ID ذکر شده باشد دیگر دسترسی به آن توسط کدهای جاوا اسکریپت مقدور نخواهد بود. بنابراین از ClientID جهت دریافت ID نهایی رندر شده توسط ASP.Net کمک می‌گیریم.

در اینجا MaskFormula مقداری است که هنگام افزودن کنترل به صفحه می‌توان تعریف کرد.

[Description("MaskedEdit Formula such as 99/99/9999")]
[Bindable(true), Category("MaskedEdit"), DefaultValue(0)]
public string MaskFormula
{
get
{
if (ViewState["MaskFormula"] == null) ViewState["MaskFormula"] = "99/99/9999";
return (string)ViewState["MaskFormula"];
}
set { ViewState["MaskFormula"] = value; }

}

این خاصیت public هنگام نمایش در Visual studio به شکل زیر درخواهد آمد:



نکته مهم: در اینجا حتما باید از view state جهت نگهداری مقدار این خاصیت استفاده کرد تا در حین post back ها مقادیر انتساب داده شده حفظ شوند.

اکنون پروژه را کامپایل کنید. برای افزودن کنترل ایجاد شده به toolbox می‌توان مطابق تصویر عمل کرد:



نکته: برای افزودن آیکون به کنترل (جهت نمایش در نوار ابزار) باید: الف) تصویر مورد نظر از نوع bmp باشد با اندازه 16 در16 pixel . ب) باید آنرا به پروژه افزود و build action آن را به Embedded Resource تغییر داد. سپس آنرا در فایل AssemblyInfo.cs پروژه نیز تعریف کرد (به صورت زیر).

[assembly: System.Web.UI.WebResource("MaskedEditCtrl.MaskedEdit.bmp", "img/bmp")]

کنترل ما پس از افزوده شدن، شکل زیر را خواهد داشت:


جهت دریافت سورس کامل و فایل بایناری این کنترل، اینجا کلیک کنید.


مطالب دوره‌ها
تبدیلگر تاریخ شمسی برای AutoMapper
فرض کنید مدل معادل با جدول بانک اطلاعاتی ما چنین ساختاری را دارد:
public class User
{
    public int Id { set; get; }
    public string Name { set; get; }
    public DateTime RegistrationDate { set; get; }
}
و ViewModel ایی که قرار است به کاربر نمایش داده شود این ساختار را دارد:
public class UserViewModel
{
    public int Id { set; get; }
    public string Name { set; get; }
    public string RegistrationDate { set; get; }
}
در اینجا می‌خواهیم حین تبدیل User به UserViewModel، تاریخ میلادی به صورت خودکار، تبدیل به یک رشته‌ی شمسی شود. برای مدیریت یک چنین سناریوهایی توسط AutoMapper، امکان نوشتن تبدیلگرهای سفارشی نیز پیش بینی شده‌است.


تبدیلگر سفارشی تاریخ میلادی به شمسی مخصوص AutoMapper

در ذیل یک تبدیلگر سفارشی مخصوص AutoMapper را با پیاده سازی اینترفیس ITypeConverter آن ملاحظه می‌کنید:
public class DateTimeToPersianDateTimeConverter : ITypeConverter<DateTime, string>
{
    private readonly string _separator;
    private readonly bool _includeHourMinute;
 
    public DateTimeToPersianDateTimeConverter(string separator = "/", bool includeHourMinute = true)
    {
        _separator = separator;
        _includeHourMinute = includeHourMinute;
    }
 
    public string Convert(ResolutionContext context)
    {
        var objDateTime = context.SourceValue;
        return objDateTime == null ? string.Empty : toShamsiDateTime((DateTime)context.SourceValue);
    }
 
    private string toShamsiDateTime(DateTime info)
    {
        var year = info.Year;
        var month = info.Month;
        var day = info.Day;
        var persianCalendar = new PersianCalendar();
        var pYear = persianCalendar.GetYear(new DateTime(year, month, day, new GregorianCalendar()));
        var pMonth = persianCalendar.GetMonth(new DateTime(year, month, day, new GregorianCalendar()));
        var pDay = persianCalendar.GetDayOfMonth(new DateTime(year, month, day, new GregorianCalendar()));
        return _includeHourMinute ?
            string.Format("{0}{1}{2}{1}{3} {4}:{5}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture), info.Hour.ToString("00"), info.Minute.ToString("00"))
            : string.Format("{0}{1}{2}{1}{3}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture));
    } 
}
ITypeConverter دو پارامتر جنریک را قبول می‌کند. پارامتر اول نوع ورودی و پارامتر دوم، نوع خروجی مورد انتظار است. در اینجا باید خروجی متد Convert را بر اساس آرگومان دوم ITypeConverter مشخص کرد. توسط ResolutionContext می‌توان به برای مثال context.SourceValue که معادل DateTime دریافتی است، دسترسی یافت. سپس این DateTime را بر اساس متد toShamsiDateTime تبدیل کرده و بازگشت می‌دهیم.


ثبت و معرفی تبدیلگرهای سفارشی AutoMapper

پس از تعریف یک تبدیلگر سفارشی AutoMapper، اکنون نیاز است آن‌را به AutoMapper معرفی کنیم:
public class TestProfile1 : Profile
{
    protected override void Configure()
    {
        // این تنظیم سراسری هست و به تمام خواص زمانی اعمال می‌شود
        this.CreateMap<DateTime, string>().ConvertUsing(new DateTimeToPersianDateTimeConverter()); 
        this.CreateMap<User, UserViewModel>();
     }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
جهت مدیریت بهتر نگاشت‌های AutoMapper ابتدا یک کلاس Profile را آغاز خواهیم کرد و سپس توسط متدهای CreateMap، کار معرفی نگاشت‌ها را آغاز می‌کنیم.
همانطور که مشاهده می‌کنید در اینجا دو نگاشت تعریف شده‌اند. یکی برای تبدیل User به UserViewModel و دیگری، معرفی نحوه‌ی نگاشت DateTime به string، توسط تبدیلگر سفارشی DateTimeToPersianDateTimeConverter است که به کمک متد الحاقی ConvertUsing صورت گرفته‌است.
باید دقت داشت که تنظیمات تبدیلگرهای سفارشی سراسری هستند و در کل برنامه و به تمام پروفایل‌ها اعمال می‌شوند.


بررسی خروجی تبدیلگر سفارشی تاریخ

اکنون کار استفاده از تنظیمات AutoMapper با ثبت پروفایل تعریف شده آغاز می‌شود:
Mapper.Initialize(cfg => // In Application_Start()
{
     cfg.AddProfile<TestProfile1>();
});
سپس نحوه‌ی استفاده از متد Mapper.Map همانند قبل خواهد بود:
var dbUser1 = new User
{
    Id = 1,
    Name = "Test",
    RegistrationDate = DateTime.Now.AddDays(-10)
};
 
var uiUser = new UserViewModel();

Mapper.Map(source: dbUser1, destination: uiUser);
در اینجا در حین کار تبدیل و نگاشت dbUser به uiUser، زمانیکه AutoMapper به هر خاصیت DateTime ایی می‌رسد، مقدار آن‌را با توجه به تبدیلگر سفارشی تاریخی که به آن معرفی کردیم، تبدیل به معادل رشته‌ای شمسی می‌کند.


نوشتن تبدیلگرهای غیر سراسری

همانطور که عنوان شد، معرفی تبدیلگرها به AutoMapper سراسری است و در کل برنامه اعمال می‌شود. اگر نیاز است فقط برای یک مدل خاص و یک خاصیت خاص آن تبدیلگر نوشته شود، باید نگاشت مورد نظر را به صورت ذیل تعریف کرد:
this.CreateMap<User, UserViewModel>()
             .ForMember(userViewModel => userViewModel.RegistrationDate,
                        opt => opt.ResolveUsing(src =>
                        {
                             var dt = src.RegistrationDate;
                             return dt.ToShortDateString();
                        }));
اینبار در همان کلاس پروفایل ابتدای بحث، نگاشت User به ViewModel آن با کمک متد ForMember، سفارشی سازی شده‌است. در اینجا عنوان شده‌است که اگر به خاصیت ویژه‌ی RegistrationDate رسیدی، مقدار آن‌را با توجه به فرمولی که مشخص شده، محاسبه کرده و بازگشت بده. این تنظیم خصوصی است و به کل برنامه اعمال نمی‌شود.


خصوصی سازی تبدیلگرها با تدارک موتورهای نگاشت اختصاصی

اگر می‌خواهید تنظیمات TestProfile1 به کل برنامه اعمال نشود، نیاز است یک MappingEngine جدید و مجزای از MappingEngine سراسری AutoMapper را ایجاد کرد:
var configurationStore = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers);
configurationStore.AddProfile<TestProfile1>();
var mapper = new MappingEngine(configurationStore);
mapper.Map(source: dbUser1, destination: uiUser);
به صورت پیش فرض و در پشت صحنه، متد Mapper.Map از یک MappingEngine سراسری استفاده می‌کند. اما می‌توان در یک برنامه چندین MappingEngine مجزا داشت که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample02.zip
مطالب
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
پیشنیاز این بحث مطالعه‌ی مطلب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال اول اکتفاء خواهد شد.


تغییرات مورد نیاز جهت فعال سازی ویرایش، حذف و افزودن رکوردهای jqGrid

می‌خواهیم در بدو نمایش گرید، یک ستون خاص دارای دکمه‌های ویرایش و حذف ظاهر شوند:


برای اینکار تنها کافی است در انتهای ستون‌های تعریف شده، یک ستون خاص را با formatter مساوی actions ایجاد کنیم:
                colModel: [
                    {
                        // سایر ستون‌ها
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false, formatter: 'actions',
                        formatoptions: {
                            keys: true
                        }
                    }
                ],
برای اینکه دکمه‌های ویرایش و حذف ردیف‌های آن عمل کنند:


نیاز است تعاریف سایر ستون‌هایی را که باید قابلیت ویرایش داشته باشند، به نحو ذیل تغییر دهیم:
                colModel: [
                    {
                        name: 'Id', index: 'Id', align: 'right', width: 70,
                        editable: false
                    },
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 100,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 110,
                        editable: true, edittype: 'select',
                        editoptions: {
                            dataUrl: '@Url.Action("SuppliersSelect","Home")'
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Category.Id', index: 'Category.Id', align: 'right', width: 110,
                        editable: true, edittype: 'select',
                        editoptions: {
                            dataUrl: '@Url.Action("CategoriesSelect","Home")'
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Price', index: 'Price', align: 'center', width: 100,
                        formatter: 'currency',
                        formatoptions:
                        {
                            decimalSeparator: '.',
                            thousandsSeparator: ',',
                            decimalPlaces: 2,
                            prefix: '$'
                        },
                        editable: true, edittype: 'text',
                        editrules: {
                            required: true,
                            number: true,
                            minValue: 0
                        }
                    },
                    {
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false, formatter: 'actions',
                        formatoptions: {
                            keys: true
                        }
                    }
                ],
- در اینجا هر ستونی که دارای خاصیت editable مساوی true است، قابلیت ویرایش پیدا می‌کند.
- edittype آن بیانگر کنترلی است که باید حین ویرایش آن سلول خاص ظاهر شود. برای مثال اگر text باشد، یک text box و اگر مانند حالت Supplier.Id مساوی select تعریف شود، یک drop down را ظاهر خواهد کرد. برای مقدار دهی این drop down می‌توان editoptions و سپس dataUrl آن‌را مقدار دهی نمود.
        public ActionResult SuppliersSelect()
        {
            var list = ProductDataSource.LatestProducts;
            var suppliers = list.Select(x => new SelectListItem
            {
                Text = x.Supplier.CompanyName,
                Value = x.Supplier.Id.ToString(CultureInfo.InvariantCulture)
            }).ToList();
            return PartialView("_SelectPartial", suppliers);
        }
در مثال فوق، این dataUrl به اکشن متد SuppliersSelect اشاره می‌کند که نهایتا لیستی از تولید کننده‌ها را توسط partial view ذیل بازگشت می‌دهد:
 @model IList<SelectListItem>

@Html.DropDownList("srch", Model)
در کل مقادیر قابل تنظیم در اینجا شامل  text، textarea، select، checkbox، password، button، image، file و custom هستند.
- خاصیت editrules، برای مباحث اعتبارسنجی اطلاعات ورودی توسط کاربر پیش بینی شده‌است. برای مثال اگر required: true در آن تنظیم شود، کاربر مجبور به تکمیل این سلول خاص خواهد بود. در اینجا خواصی مانند number و integer از نوع bool، خاصیت‌های minValue و maxValue از نوع عددی، email, url, date, time از نوع bool و custom قابل تنظیم است (مثال‌های حالت custom را در منابع انتهای بحث می‌توانید مطالعه کنید).
- پس از اینکه مشخص شدند کدامیک از ستون‌ها باید قابلیت ویرایش داشته باشند، مسیری که باید اطلاعات نهایی را به سرور ارسال کند، توسط خاصیت editurl مشخص می‌شود:
$('#list').jqGrid({
   caption: "آزمایش چهارم",
   //url from wich data should be requested
   url: '@Url.Action("GetProducts","Home")',
   //url for edit operation
   editurl: '@Url.Action("EditProduct","Home")',
اکشن متد متناظر با این آدرس یک چنین شکلی را می‌تواند داشته باشد:
        [HttpPost]
        public ActionResult EditProduct(Product postData)
        {
            //todo: Edit product based on postData

            return Json(true);
        }
- تعاریف مسیرهای ارسال اطلاعات Add و Delete، در قسمت تنظیمات navGrid باید ذکر شوند:
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: false, search: false },
                //edit options
                {},
                //add options
                { width: 'auto', url: '@Url.Action("AddProduct","Home")' },
                //delete options
                { url: '@Url.Action("DeleteProduct","Home")' }
                );
امضای این اکشن متدها نیز بسیار شبیه به اکشن متد ویرایش است:
        [HttpPost]
        public ActionResult DeleteProduct(string id)
        {
            //todo: Delete product
            return Json(true);
        }

        [HttpPost]
        public ActionResult AddProduct(Product postData)
        {
            //todo: Add product to repository
            return Json(true);
        }
- حالت ویرایش و حذفی که تا اینجا بررسی شد (ستون actions)، جزو خواص توکار این گرید است. اگر بخواهیم آن‌ها را دستی فعال کنیم (جهت اطلاعات عمومی) می‌توان از فراخوانی متد ذیل نیز کمک گرفت:
        var lastSel;
        function inlineEdit() {
            $('input[name=rdEditApproach]').attr('disabled', true);
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: false, search: false },
                //edit options
                {},
                //add options
                { width: 'auto', url: '@Url.Action("AddProduct","Home")' },
                //delete options
                { url: '@Url.Action("DeleteProduct","Home")' }
                );
            //add onSelectRow event to support inline edit
            $('#list').setGridParam({
                onSelectRow: function (id) {
                    if (id && id != lastSel) {
                        //save changes in row
                        $('#list').saveRow(lastSel, false);
                        lastSel = id;
                    }
                    //trigger inline edit for row
                    $('#list').editRow(id, true);
                }
            });
        };
در اینجا ابتدا همان تنظیمات مسیرهای Add و Delete انجام شده‌است. سپس با فراخوانی دستی متد editRow در زمان کلیک بر روی یک ردیف، همان کاری را که ستون actions در جهت فعال سازی خودکار حالت ویرایش سلول‌ها انجام می‌دهد، می‌توان شبیه سازی کرد. متد saveRow نیز کار ارسال اطلاعات تغییر کرده را به سرور انجام می‌دهد.
- برای فعال سازی خودکار فرم‌های افزودن رکوردها و یا ویرایش ردیف‌های موجود می‌توان از فراخوانی متد formEdit ذیل کمک گرفت:
        function formEdit() {
            $('input[name=rdEditApproach]').attr('disabled', true);
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    width: 'auto', checkOnUpdate: true, checkOnSubmit: true,
                    beforeShowForm: function (form) {
                        centerDialog(form, $('#list'));
                    }
                },
                //add options
                {
                    width: 'auto', url: '@Url.Action("AddProduct","Home")',
                    reloadAfterSubmit: false, checkOnUpdate: true, checkOnSubmit: true,
                    beforeShowForm: function (form) {
                        centerDialog(form, $('#list'));
                    }
                },
                //delete options
                {
                    url: '@Url.Action("DeleteProduct","Home")', reloadAfterSubmit: false
                })
            .jqGrid('navButtonAdd', "#pager", {
                caption: "حذف ردیف‌های انتخابی", title: "Delete Toolbar", buttonicon: 'ui-icon ui-icon-trash',
                onClickButton: function () {
                    var idsList = jQuery("#list").jqGrid('getGridParam', 'selarrrow');
                    alert(idsList);
                    //jQuery("#list").jqGrid('delGridRow',idsList,{reloadAfterSubmit:false});
                }
            });
        };

        function centerDialog(form, grid) {
            var dlgDiv = $("#editmod" + grid[0].id);
            var parentDiv = dlgDiv.parent(); // div#gbox_list
            var dlgWidth = dlgDiv.width();
            var parentWidth = parentDiv.width();
            var dlgHeight = dlgDiv.height();
            var parentHeight = parentDiv.height();
            var parentTop = parentDiv.offset().top;
            var parentLeft = parentDiv.offset().left;
            dlgDiv[0].style.top =  Math.round(  parentTop  + (parentHeight-dlgHeight)/2  ) + "px";
            dlgDiv[0].style.left = Math.round(  parentLeft + (parentWidth-dlgWidth  )/2 )  + "px";
        }
ابتدای تنظیمات آن، شاهد add: true, del: true, edit: true هستید. این مورد سبب می‌شود تا در فوتر گرید، سه دکمه‌ی افزودن، ویرایش و حذف ردیف‌ها ظاهر شوند:


با کلیک بر روی دکمه‌ی افزودن ردیف جدید، صفحه‌ی ذیل به صورت خودکار تولید می‌شود:


و با کلیک بر روی دکمه‌ی ویرایش ردیفی انتخاب شده، صفحه‌ی ویرایش آن ردیف به همراه مقادیر سلول‌های آن ظاهر خواهند شد:


تنظیمات قسمت‌های Add و Delete ویرایش توسط فرم‌ها، با حالت ویرایش داخل ردیفی آنچنان تفاوتی ندارد. فقط در اینجا پیش از نمایش فرم، از متد centerDialog برای نمایش صفحات افزودن و ویرایش رکوردها در وسط صفحه، استفاده شده‌است. توسط checkOnUpdate: true, checkOnSubmit: true سبب خواهیم شد تا اگر کاربر مقادیر موجود فرمی را تغییر داده‌است و سعی در بستن فرم، بدون ذخیره سازی اطلاعات کند، پیغام هشدار دهنده‌ای به او نمایش داده شود که آیا می‌خواهید تغییرات را ذخیره کنید یا خیر؟


- در انتهای متد formEdit، به کمک متد jqGrid و پارامتر navButtonAdd یک دکمه‌ی سفارشی را نیز اضافه کرده‌ایم. اگر به ستون پس از شماره‌های خودکار ردیف‌ها، در سمت راست گرید دقت کنید، یک سری chekbox قابل مشاهده هستند. برای فعال سازی خودکار آن‌ها کافی است خاصیت multiselect گرید به true تنظیم شود. اکنون برای دسترسی به این ستون‌های انتخاب شده، می‌توان از متد jqGrid به همراه پارامترهای getGridParam و selarrrow استفاده کرد. خروجی آن، لیست idهای ستون‌ها است.


برای مطالعه بیشتر

Common Editing Properties
Inline Editing
Form Editing
Cell Editing


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid04.zip
نظرات مطالب
EF Code First #1
در مورد کانکشن استرینگ، ایجاد بانک اطلاعاتی و غیره در قسمت‌های بعدی بیشتر توضیح داده شده. اگر تعاریف رشته اتصالی شما به این نحو باشد:
<connectionStrings>
    <clear/>
    <add
       name="ProductContext"
       connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
       providerName="System.Data.SqlClient"
      />
</connectionStrings>
به این معنا است که کلاس Context شما به نحو زیر تعریف شده است:
public class ProductContext : DbContext
بنابراین جزو قرار دادهای توکار EF Code first است که name ذکر شده در قسمت تعریف رشته اتصالی در فایل کانفیگ، با نام کلاس مشتق شده از DbContext یکی باشد.
با این تعاریف باید برنامه کار کند (البته بر اساس نام کلاس‌های برنامه شما).
ضمنا login failed به این معنا است که رشته اتصالی اشتباه تعریف شده است. رشته فوق به یک بانک اطلاعاتی sql server و به وهله پیش فرض آن اشاره می‌کند و از نوع windows authentication است. این موارد را باید بر اساس تنظیمات سیستم خودتون تغییر بدید.

مطالب
روش آپلود فایل‌ها به همراه اطلاعات یک مدل در برنامه‌های Blazor WASM 5x
از زمان Blazor 5x، امکان آپلود فایل به صورت استاندارد به Blazor اضافه شده‌است که نمونه‌ی Blazor Server آن‌را پیشتر در مطلب «Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر» مطالعه کردید. در تکمیل آن، روش آپلود فایل‌ها در برنامه‌های WASM را نیز بررسی خواهیم کرد. این برنامه از نوع hosted است؛ یعنی توسط دستور dotnet new blazorwasm --hosted ایجاد شده‌است و به صورت خودکار دارای سه بخش Client، Server و Shared است.



معرفی مدل ارسالی برنامه سمت کلاینت

فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کرده‌ایم:
using System.ComponentModel.DataAnnotations;

namespace BlazorWasmUpload.Shared
{
    public class User
    {
        [Required]
        public string Name { get; set; }

        [Required]
        [Range(18, 90)]
        public int Age { get; set; }
    }
}

ساختار کنترلر Web API دریافت کننده‌ی مدل برنامه

در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
namespace BlazorWasmUpload.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class FilesController : ControllerBase
    {
        [HttpPost]
        public async Task<IActionResult> CreateUser(
            [FromForm] User userModel,
            [FromForm] IList<IFormFile> inputFiles = null)
یعنی در سمت Web API، قرار است اطلاعات مدل User و همچنین لیستی از فایل‌های آپلودی (احتمالی و اختیاری) را یکجا و در طی یک عملیات Post، دریافت کنیم. در اینجا نام پارامترهایی را هم که انتظار داریم، دقیقا userModel و inputFiles هستند. همچنین فایل‌های آپلودی باید بتوانند ساختار IFormFile استاندارد ASP.NET Core را تشکیل داده و به صورت خودکار به پارامترهای تعریف شده، bind شوند. به علاوه content-type مورد انتظار هم FromForm است.


ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایل‌های انتخابی کاربر

کدهای کامل سرویسی که می‌تواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorWasmUpload.Client.Services
{
    public interface IFilesManagerService
    {
        Task<HttpResponseMessage> PostModelWithFilesAsync<T>(string requestUri,
            IEnumerable<IBrowserFile> browserFiles,
            string fileParameterName,
            T model,
            string modelParameterName);
    }

    public class FilesManagerService : IFilesManagerService
    {
        private readonly HttpClient _httpClient;

        public FilesManagerService(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<HttpResponseMessage> PostModelWithFilesAsync<T>(
            string requestUri,
            IEnumerable<IBrowserFile> browserFiles,
            string fileParameterName,
            T model,
            string modelParameterName)
        {
            var requestContent = new MultipartFormDataContent();
            requestContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");

            if (browserFiles?.Any() == true)
            {
                foreach (var file in browserFiles)
                {
                    var stream = file.OpenReadStream(maxAllowedSize: 512000 * 1000);
                    requestContent.Add(content: new StreamContent(stream, (int)file.Size), name: fileParameterName, fileName: file.Name);
                }
            }

            requestContent.Add(
                content: new StringContent(JsonSerializer.Serialize(model), Encoding.UTF8, "application/json"),
                name: modelParameterName);

            var result = await _httpClient.PostAsync(requestUri, requestContent);
            result.EnsureSuccessStatusCode();
            return result;
        }
    }
}
توضیحات:
- کامپوننت استاندارد InputFiles در Blazor Wasm، می‌تواند لیستی از IBrowserFile‌های انتخابی توسط کاربر را در اختیار ما قرار دهد.
- fileParameterName، همان نام پارامتر "inputFiles" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شده‌است.
- model جنریک، برای نمونه وهله‌ای از شیء User است که به یک فرم Blazor متصل است.
- modelParameterName، همان نام پارامتر "userModel" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شده‌است.

- در ادامه یک MultipartFormDataContent را تشکیل داده‌ایم. توسط این ساختار می‌توان فایل‌ها و اطلاعات یک مدل را به صورت یکجا جمع آوری و به سمت سرور ارسال کرد. به این content ویژه، ابتدای لیستی از new StreamContent‌ها را اضافه می‌کنیم. این streamها توسط متد OpenReadStream هر IBrowserFile دریافتی از کامپوننت InputFile، تشکیل می‌شوند. متد OpenReadStream به صورت پیش‌فرض فقط فایل‌هایی تا حجم 500 کیلوبایت را پردازش می‌کند و اگر فایلی حجیم‌تر را به آن معرفی کنیم، یک استثناء را صادر خواهد کرد. به همین جهت می‌توان توسط پارامتر maxAllowedSize آن، این مقدار پیش‌فرض را تغییر داد.

- در اینجا مدل برنامه به صورت JSON به عنوان یک new StringContent اضافه شده‌است. مزیت کار کردن با JsonSerializer.Serialize استاندارد، ساده شدن برنامه و عدم درگیری با مباحث Reflection و خواندن پویای اطلاعات مدل جنریک است. اما در ادامه مشکلی را پدید خواهد آورد! این رشته‌ی ارسالی به سمت سرور، به صورت خودکار به یک مدل، Bind نخواهد شد و باید برای آن یک model-binder سفارشی را بنویسیم. یعنی این رشته‌ی new StringContent را در سمت سرور دقیقا به صورت یک رشته معمولی می‌توان دریافت کرد و نه حالت دیگری و مهم نیست که اکنون به صورت JSON ارسال می‌شود؛ چون MultipartFormDataContent ویژه‌ای را داریم، model-binder پیش‌فرض ASP.NET Core، انتظار یک شیء خاص را در این بین ندارد.

- تنظیم "form-data" را هم به عنوان Headers.ContentDisposition مشاهده می‌کنید. بدون وجود آن، ویژگی [FromForm] سمت Web API، از پردازش درخواست جلوگیری خواهد کرد.

- در آخر توسط متد PostAsync، این اطلاعات جمع آوری شده، به سمت سرور ارسال خواهند شد.

پس از تهیه‌ی سرویس ویژه‌ی فوق که می‌تواند اطلاعات فایل‌ها و یک مدل را به صورت یکجا به سمت سرور ارسال کند، اکنون نوبت به ثبت و معرفی آن به سیستم تزریق وابستگی‌ها در فایل Program.cs برنامه‌ی کلاینت است:
namespace BlazorWasmUpload.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...

            builder.Services.AddScoped<IFilesManagerService, FilesManagerService>();

            // ...
        }
    }
}


تکمیل فرم ارسال اطلاعات مدل و فایل‌های همراه آن در برنامه‌ی Blazor WASM

در ادامه پس از تشکیل IFilesManagerService، نوبت به استفاده‌ی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر می‌دهیم:
@code
{
    IReadOnlyList<IBrowserFile> SelectedFiles;
    User UserModel = new User();
    bool isProcessing;
    string UploadErrorMessage;
در اینجا فیلدهای مورد استفاده‌ی در فرم برنامه مشخص شده‌اند:
- SelectedFiles همان لیست فایل‌های انتخابی توسط کاربر است.
- UserModel شیءای است که به EditForm جاری متصل خواهد شد.
- توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص می‌کنیم.
- UploadErrorMessage، خطای احتمالی انتخاب فایل‌ها مانند «فقط تصاویر را انتخاب کنید» را تعریف می‌کند.

بر این اساس، فرمی را که در تصویر ابتدای بحث مشاهده کردید، به صورت زیر تشکیل می‌دهیم:
@page "/"

@using System.IO
@using BlazorWasmUpload.Shared
@using BlazorWasmUpload.Client.Services

@inject IFilesManagerService FilesManagerService

<h3>Post a model with files</h3>

<EditForm Model="UserModel" OnValidSubmit="CreateUserAsync">
    <DataAnnotationsValidator />
    <div>
        <label>Name</label>
        <InputText @bind-Value="UserModel.Name"></InputText>
        <ValidationMessage For="()=>UserModel.Name"></ValidationMessage>
    </div>
    <div>
        <label>Age</label>
        <InputNumber @bind-Value="UserModel.Age"></InputNumber>
        <ValidationMessage For="()=>UserModel.Age"></ValidationMessage>
    </div>
    <div>
        <label>Photos</label>
        <InputFile multiple disabled="@isProcessing" OnChange="OnInputFileChange" />
        @if (!string.IsNullOrWhiteSpace(UploadErrorMessage))
        {
            <div>
                @UploadErrorMessage
            </div>
        }
        @if (SelectedFiles?.Count > 0)
        {
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Size (bytes)</th>
                        <th>Last Modified</th>
                        <th>Type</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var selectedFile in SelectedFiles)
                    {
                        <tr>
                            <td>@selectedFile.Name</td>
                            <td>@selectedFile.Size</td>
                            <td>@selectedFile.LastModified</td>
                            <td>@selectedFile.ContentType</td>
                        </tr>
                    }
                </tbody>
            </table>
        }
    </div>
    <div>
        <button disabled="@isProcessing">Create user</button>
    </div>
</EditForm>
توضیحات:
- UserModel که وهله‌ی از شیء اشتراکی User است، به EditForm متصل شده‌است.
- سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت می‌کنیم.
- InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر می‌کند. پس از انتخاب فایل‌ها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
    private void OnInputFileChange(InputFileChangeEventArgs args)
    {
        var files = args.GetMultipleFiles(maximumFileCount: 15);
        if (args.FileCount == 0 || files.Count == 0)
        {
            UploadErrorMessage = "Please select a file.";
            return;
        }

        var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" };
        if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase)))
        {
            UploadErrorMessage = "Please select .jpg/.jpeg/.png files only.";
            return;
        }

        SelectedFiles = files;
        UploadErrorMessage = string.Empty;
    }
- در اینجا امضای متد رویداد گردان OnChange را مشاهده می‌کنید. توسط متد GetMultipleFiles می‌توان لیست فایل‌های انتخابی توسط کاربر را دریافت کرد. نیاز است پارامتر maximumFileCount آن‌را نیز تنظیم کنیم تا دقیقا مشخص شود چه تعداد فایلی مدنظر است؛ بیش از آن، یک استثناء را صادر می‌کند.
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش می‌دهیم.
- در پایان این متد، لیست فایل‌های دریافتی را به فیلد SelectedFiles انتساب می‌دهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.

مرحله‌ی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
    private async Task CreateUserAsync()
    {
        try
        {
            isProcessing = true;
            await FilesManagerService.PostModelWithFilesAsync(
                        requestUri: "api/Files/CreateUser",
                        browserFiles: SelectedFiles,
                        fileParameterName: "inputFiles",
                        model: UserModel,
                        modelParameterName: "userModel");
            UserModel = new User();
        }
        finally
        {
            isProcessing = false;
            SelectedFiles = null;
        }
    }
- در اینجا زمانیکه isProcessing به true تنظیم می‌شود، دکمه‌ی ارسال اطلاعات، غیرفعال خواهد شد؛ تا از کلیک چندباره‌ی بر روی آن جلوگیری شود.
- سپس روش استفاده‌ی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده می‌کنید که اطلاعات فایل‌ها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال می‌کند.
- در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آماده‌ی دریافت اطلاعات بعدی می‌کنیم.


تکمیل کنترلر Web API دریافت کننده‌ی مدل برنامه

در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کننده‌ی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آن‌را مشاهده می‌کنید:
using System.IO;
using Microsoft.AspNetCore.Mvc;
using BlazorWasmUpload.Shared;
using Microsoft.AspNetCore.Hosting;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using BlazorWasmUpload.Server.Utils;
using System.Linq;

namespace BlazorWasmUpload.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class FilesController : ControllerBase
    {
        private const int MaxBufferSize = 0x10000;

        private readonly IWebHostEnvironment _webHostEnvironment;
        private readonly ILogger<FilesController> _logger;

        public FilesController(
            IWebHostEnvironment webHostEnvironment,
            ILogger<FilesController> logger)
        {
            _webHostEnvironment = webHostEnvironment;
            _logger = logger;
        }

        [HttpPost]
        public async Task<IActionResult> CreateUser(
            //[FromForm] string userModel, // <-- this is the actual form of the posted model
            [ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
            [FromForm] IList<IFormFile> inputFiles = null)
        {
            /*var user = JsonSerializer.Deserialize<User>(userModel);
            _logger.LogInformation($"userModel.Name: {user.Name}");
            _logger.LogInformation($"userModel.Age: {user.Age}");*/

            _logger.LogInformation($"userModel.Name: {userModel.Name}");
            _logger.LogInformation($"userModel.Age: {userModel.Age}");

            var uploadsRootFolder = Path.Combine(_webHostEnvironment.WebRootPath, "Files");
            if (!Directory.Exists(uploadsRootFolder))
            {
                Directory.CreateDirectory(uploadsRootFolder);
            }

            if (inputFiles?.Any() == true)
            {
                foreach (var file in inputFiles)
                {
                    if (file == null || file.Length == 0)
                    {
                        continue;
                    }

                    var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                    using var fileStream = new FileStream(filePath,
                                                            FileMode.Create,
                                                            FileAccess.Write,
                                                            FileShare.None,
                                                            MaxBufferSize,
                                                            useAsync: true);
                    await file.CopyToAsync(fileStream);
                    _logger.LogInformation($"Saved file: {filePath}");
                }
            }

            return Ok();
        }
    }
}
نکات تکمیلی این کنترلر را در مطلب «بررسی روش آپلود فایل‌ها در ASP.NET Core» می‌توانید مطالعه کنید و از این لحاظ هیچ نکته‌ی جدیدی را به همراه ندارد؛ بجز پارامتر userModel آن:
[ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
همانطور که عنوان شد، userModel ارسالی به سمت سرور چون به همراه تعدادی فایل است، به صورت خودکار به شیء User نگاشت نخواهد شد. به همین جهت نیاز است model-binder سفارشی زیر را برای آن تهیه کرد:
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace BlazorWasmUpload.Server.Utils
{
    public class JsonModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult != ValueProviderResult.None)
            {
                bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

                var valueAsString = valueProviderResult.FirstValue;
                var result = JsonSerializer.Deserialize(valueAsString, bindingContext.ModelType);
                if (result != null)
                {
                    bindingContext.Result = ModelBindingResult.Success(result);
                    return Task.CompletedTask;
                }
            }

            return Task.CompletedTask;
        }
    }
}
در اینجا مقدار رشته‌ای پارامتر مزین شده‌ی توسط JsonModelBinder فوق، توسط متد استاندارد JsonSerializer.Deserialize تبدیل به یک شیء شده و به آن پارامتر انتساب داده می‌شود. اگر نخواهیم از این model-binder سفارشی استفاده کنیم، ابتدا باید پارامتر دریافتی را رشته‌ای تعریف کنیم و سپس خودمان کار فراخوانی متد JsonSerializer.Deserialize را انجام دهیم:
[HttpPost]
public async Task<IActionResult> CreateUser(
            [FromForm] string userModel, // <-- this is the actual form of the posted model
            [FromForm] IList<IFormFile> inputFiles = null)
{
  var user = JsonSerializer.Deserialize<User>(userModel);


یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمی‌شود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه می‌شود و این API استاندارد، هنوز از streaming پشتیبانی نمی‌کند . حتی ممکن است با کمی جستجو به راه‌حل‌هایی که سعی کرده‌اند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راه‌حل‌ها تنها کاری را که انجام می‌دهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده می‌شود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmUpload.zip
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت هشتم- تعریف سطوح دسترسی پیچیده
تعریف نقش‌ها، ساده‌ترین روش محدود کردن دسترسی به منابع است؛ برای نمونه مشخص می‌کنیم که کاربران دارای نقش PayingUser، امکان سفارش دادن نگارش قاب شده‌ی تصاویر را داشته باشند. اما می‌خواهیم منطق دسترسی به منابع مختلف را پیچیده‌تر کنیم. برای مثال می‌خواهیم بررسی کنیم اگر منبعی واقعا متعلق به کاربر جاری سیستم است، به آن دسترسی داشته باشد. برای مثال هرچند کاربر جاری دارای نقش PayingUser است، اما آیا باید اجازه‌ی ویرایش تصاویر تمام کاربران، برای او صادر شده باشد؟ برای پیاده سازی یک چنین موارد پیچیده‌ای که فراتر از مفهوم نقش‌ها هستند، ویژگی جدیدی به نام Authorization policies به ASP.NET Core اضافه شده‌است که آن‌را در این قسمت بر اساس امکانات IdentityServer 4 بررسی می‌کنیم.


مقایسه تعریف سطوح دسترسی «مبتنی بر نقش‌ها» با سطوح دسترسی «مبتنی بر سیاست‌های امنیتی»

- در سطوح دسترسی «مبتنی بر نقش‌ها»
یک‌سری نقش از پیش تعریف شده وجود دارند؛ مانند PayingUser و یا FreeUser که کاربر توسط هر نقش، به یکسری دسترسی‌های خاص نائل می‌شود. برای مثال PayingUser می‌تواند نگارش قاب شده‌ی تصاویر را سفارش دهد و یا تصویری را به سیستم اضافه کند.

- در سطوح دسترسی «مبتنی بر سیاست‌های امنیتی»
سطوح دسترسی بر اساس یک سری سیاست که بیانگر ترکیبی از منطق‌های دسترسی هستند، اعطاء می‌شوند. این منطق‌ها نیز از طریق ترکیب User Claims حاصل می‌شوند و می‌توانند منطق‌های پیچیده‌تری را به همراه داشته باشند. برای مثال اگر کاربری از کشور A است و نوع اشتراک او B است و اگر در بین یک بازه‌ی زمانی خاصی متولد شده باشد، می‌تواند به منبع خاصی دسترسی پیدا کند. به این ترتیب حتی می‌توان نیاز به ترکیب چندین نقش را با تعریف یک سیاست امنیتی جدید جایگزین کرد. به همین جهت نسبت به روش بکارگیری مستقیم کار با نقش‌ها ترجیح داده می‌شود.


جایگزین کردن بررسی سطوح دسترسی توسط نقش‌ها با روش بکارگیری سیاست‌های دسترسی

در ادامه می‌خواهیم بجای بکارگیری مستقیم نقش‌ها جهت محدود کردن دسترسی به قسمت‌های خاصی از برنامه‌ی کلاینت، تنها کاربرانی که از کشور خاصی وارد شده‌اند و نیز سطح اشتراک خاصی را دارند، بتوانند دسترسی‌های ویژه‌ای داشته باشند؛ چون برای مثال امکان ارسال مستقیم تصاویر قاب شده را به کشور دیگری نداریم.

تنظیم User Claims جدید در برنامه‌ی IDP
برای تنظیم این سیاست امنیتی جدید، ابتدا دو claim جدید subscriptionlevel و country را به خواص کاربران در کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                    Username = "User 1",
                    // ...

                    Claims = new List<Claim>
                    {
    // ...
                        new Claim("subscriptionlevel", "PayingUser"),
                        new Claim("country", "ir")
                    }
                },
                new TestUser
                {
                    Username = "User 2",
// ...

                    Claims = new List<Claim>
                    {
    // ...
                        new Claim("subscriptionlevel", "FreeUser"),
                        new Claim("country", "be")
                    }
                }
            };
        }
سپس باید تعاریف این claims جدید را به متد GetIdentityResources افزود تا به صورت scopeهای جدید از طرف کلاینت‌ها قابل درخواست باشند و چون این claimها استاندارد نیستند، برای تعریف آن‌ها از IdentityResource استفاده می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // identity-related resources (scopes)
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
   // ...     
                new IdentityResource(
                    name: "country",
                    displayName: "The country you're living in",
                    claimTypes: new List<string> { "country" }),
                new IdentityResource(
                    name: "subscriptionlevel",
                    displayName: "Your subscription level",
                    claimTypes: new List<string> { "subscriptionlevel" })
            };
        }
همچنین باید مطمئن شد که کلاینت مدنظر ما قادر است این scopeهای تعریف شده را درخواست کند و IDP مجاز است تا آن‌ها را بازگشت دهد. برای این منظور آن‌ها را به لیست AllowedScopes تعریف کلاینت، اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
// ...
                    AllowedScopes =
                    {
    // ...
                        "country",
                        "subscriptionlevel"
                    }
// ...
                }
             };
        }
    }

استفاده‌ی از User Claims جدید در برنامه‌ی MVC Client
در ادامه به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client مراجعه کرده و دو scope جدیدی را که در سمت IDP تعریف کردیم، در اینجا در تنظیمات متد AddOpenIdConnect، درخواست می‌دهیم:
options.Scope.Add("subscriptionlevel");
options.Scope.Add("country");
به این ترتیب برنامه‌ی کلاینت می‌تواند دسترسی به این دو claim جدید را از طریق IDP، پیدا کند.
البته همانطور که در قسمت‌های قبل نیز ذکر شد، اگر claim ای در لیست نگاشت‌های تنظیمات میان‌افزار OpenID Connect مایکروسافت نباشد، آن‌را در لیست this.User.Claims ظاهر نمی‌کند. به همین جهت همانند claim role که پیشتر MapUniqueJsonKey را برای آن تعریف کردیم، نیاز است برای این دو claim نیز نگاشت‌های لازم را به سیستم افزود:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
options.ClaimActions.MapUniqueJsonKey(claimType: "subscriptionlevel", jsonKey: "subscriptionlevel");
options.ClaimActions.MapUniqueJsonKey(claimType: "country", jsonKey: "country");

ایجاد سیاست‌های دسترسی در برنامه‌ی MVC Client

برای تعریف یک سیاست دسترسی جدید در کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client، به متد ConfigureServices آن مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthorization(options =>
            {
                options.AddPolicy(
                   name: "CanOrderFrame",
                   configurePolicy: policyBuilder =>
                    {
                        policyBuilder.RequireAuthenticatedUser();
                        policyBuilder.RequireClaim(claimType: "country", requiredValues: "ir");
                        policyBuilder.RequireClaim(claimType: "subscriptionlevel", requiredValues: "PayingUser");
                    });
            });
در اینجا نحوه‌ی تعریف یک Authorization Policy جدید را مشاهده می‌کنید. ابتدا یک نام برای آن تعریف می‌شود که در قسمت‌های دیگر برنامه جهت ارجاع به آن مورد استفاده قرار می‌گیرد. سپس تنظیمات این سیاست دسترسی جدید را مشاهده می‌کنید که در آن نیاز است کاربر مدنظر حتما اعتبارسنجی شده باشد. از کشور ir بوده و همچنین سطح اشتراک او PayingUser باشد. در اینجا پارامتر requiredValues، یک آرایه را می‌پذیرد. بنابراین اگر برای مثال کشورهای دیگری نیز مدنظر هستند، می‌توان لیست آن‌ها را در اینجا اضافه کرد.
به علاوه policyBuilder شامل متد RequireRole نیز هست. به همین جهت است که این روش تعریف سطوح دسترسی، روش قدیمی مبتنی بر نقش‌ها را جایگزین کرده و در برگیرنده‌ی آن نیز می‌شود؛ چون در این سیستم، role نیز تنها یک claim است، مانند country و یا subscriptionlevel فوق.


بررسی نحوه‌ی استفاده‌ی از Authorization Policy تعریف شده و جایگزین کردن آن با روش بررسی نقش‌ها

تا کنون از روش بررسی سطوح دسترسی‌ها بر اساس نقش‌های کاربران در دو قسمت استفاده کرده‌ایم:
الف) اصلاح Views\Shared\_Layout.cshtml برای استفاده‌ی از Authorization Policy
در فایل Layout با بررسی نقش PayingUser، منوهای مرتبط با این نقش را فعال می‌کنیم:
@if(User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
برای جایگزین کردن آن جهت استفاده‌ی از سیاست دسترسی جدید CanOrderFrame، ابتدا نیاز است در این View به سرویس IAuthorizationService دسترسی پیدا کنیم که روش تزریق آن‌را در ذیل مشاهده می‌کنید:
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
پس از آن، روش استفاده‌ی از این سرویس را در ذیل مشاهده می‌کنید:
@if (User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
}
@if ((await AuthorizationService.AuthorizeAsync(User, "CanOrderFrame")).Succeeded)
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
اکنون لینک منوی درخواست نگارش قاب شده‌ی یک تصویر، صرفا به کاربران تامین کننده‌ی سیاست دسترسی CanOrderFrame نمایش داده می‌شود.

ب) اصلاح کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs برای استفاده‌ی از Authorization Policy
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        [Authorize(Policy = "CanOrderFrame")]
        public async Task<IActionResult> OrderFrame()
        {
در اینجا فیلتر Authorize امکان پذیرش نام یک Policy را نیز به همراه دارد.

اکنون برای آزمایش برنامه یکبار از آن خارج شده و سپس توسط اکانت User 1 که از نوع PayingUser در کشور ir است، به آن وارد شوید.
ابتدا به قسمت IdentityInformation آن وارد شوید. در اینجا لیست claims جدید را می‌توانید مشاهده کنید. همچنین لینک سفارش تصویر قاب شده نیز نمایان است و می‌توان به آدرس آن نیز وارد شد.


استفاده از سیاست‌های دسترسی در سطح برنامه‌ی Web API

در سمت برنامه‌ی Web API، در حال حاضر کاربران می‌توانند به متدهای Get ،Put و Delete ای که رکوردهای آن‌ها الزاما متعلق به آن‌ها نیست دسترسی داشته باشند. بنابراین نیاز است از ورود کاربران به متدهای تغییرات رکوردهایی که OwnerID آن‌ها با هویت کاربری آن‌ها تطابقی ندارد، جلوگیری کرد. در این حالت Authorization Policy تعریف شده نیاز دارد تا با سرویس کاربران و بانک اطلاعاتی کار کند. همچنین نیاز به دسترسی به اطلاعات مسیریابی جاری را برای دریافت ImageId دارد. پیاده سازی یک چنین سیاست دسترسی پیچیده‌ای توسط متدهای RequireClaim و RequireRole میسر نیست. خوشبختانه امکان بسط سیستم Authorization Policy با پیاده سازی یک IAuthorizationRequirement سفارشی وجود دارد. RequireClaim و RequireRole، جزو Authorization Requirementهای پیش‌فرض و توکار هستند. اما می‌توان نمونه‌های سفارشی آن‌ها را نیز پیاده سازی کرد:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace ImageGallery.WebApi.Services
{
    public class MustOwnImageRequirement : IAuthorizationRequirement
    {
    }

    public class MustOwnImageHandler : AuthorizationHandler<MustOwnImageRequirement>
    {
        private readonly IImagesService _imagesService;
        private readonly ILogger<MustOwnImageHandler> _logger;

        public MustOwnImageHandler(
            IImagesService imagesService,
            ILogger<MustOwnImageHandler> logger)
        {
            _imagesService = imagesService;
            _logger = logger;
        }

        protected override async Task HandleRequirementAsync(
            AuthorizationHandlerContext context, MustOwnImageRequirement requirement)
        {
            var filterContext = context.Resource as AuthorizationFilterContext;
            if (filterContext == null)
            {
                context.Fail();
                return;
            }

            var imageId = filterContext.RouteData.Values["id"].ToString();
            if (!Guid.TryParse(imageId, out Guid imageIdAsGuid))
            {
                _logger.LogError($"`{imageId}` is not a Guid.");
                context.Fail();
                return;
            }

            var subClaim = context.User.Claims.FirstOrDefault(c => c.Type == "sub");
            if (subClaim == null)
            {
                _logger.LogError($"User.Claims don't have the `sub` claim.");
                context.Fail();
                return;
            }

            var ownerId = subClaim.Value;
            if (!await _imagesService.IsImageOwnerAsync(imageIdAsGuid, ownerId))
            {
                _logger.LogError($"`{ownerId}` is not the owner of `{imageIdAsGuid}` image.");
                context.Fail();
                return;
            }

            // all checks out
            context.Succeed(requirement);
        }
    }
}
در پروژه‌ی ImageGallery.WebApi.Services ابتدا یک Authorization Requirement و سپس پیاده سازی کننده‌ی آن که Authorization Handler نام دارد را تعریف کرده‌ایم. این پروژه نیاز به وابستگی‌های ذیل را دارد تا کامپایل شود.
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.1.0" />
  </ItemGroup>
</Project>

پیاده سازی سیاست‌های پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
public class MustOwnImageRequirement : IAuthorizationRequirement
{
}
ابتدا نیاز است یک نیازمندی دسترسی جدید را با پیاده سازی اینترفیس IAuthorizationRequirement ارائه دهیم. این نیازمندی، خالی است و صرفا به عنوان نشانه‌ای جهت یافت AuthorizationHandler استفاده کننده‌ی از آن استفاده می‌شود. در اینجا در صورت نیاز می‌توان یک سری خاصیت اضافه را تعریف کرد تا آن‌ها را به صورت پارامترهایی ثابت به AuthorizationHandler ارسال کند.

2- پیاده سازی یک AuthorizationHandler استفاده کننده‌ی از نیازمندی دسترسی تعریف شده
که کدهای کامل آن‌را در کلاس MustOwnImageHandler مشاهده می‌کنید. کار آن با ارث بری از AuthorizationHandler شروع شده و آرگومان جنریک آن، همان نیازمندی است که پیشتر تعریف کردیم. از این آرگومان جنریک جهت یافتن خودکار AuthorizationHandler متناظر با آن توسط ASP.NET Core استفاده می‌شود. بنابراین در اینجا MustOwnImageRequirement تهیه شده صرفا کارکرد علامتگذاری را دارد.
در کلاس تهیه شده باید متد HandleRequirementAsync آن‌را بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری می‌تواند به منبع درخواستی بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت می‌کند.
در این پیاده سازی از filterContext.RouteData برای یافتن Id تصویر مورد نظر استفاده شده‌است. همچنین Id شخص جاری نیز از sub claim موجود استخراج گردیده‌است. اکنون این اطلاعات را به سرویس تصاویر ارسال می‌کنیم تا توسط متد IsImageOwnerAsync آن مشخص شود که آیا کاربر جاری سیستم، همان کاربری است که تصویر را در بانک اطلاعاتی ثبت کرده‌است؟ اگر بله، با فراخوانی context.Succeed به سیستم Authorization اعلام خواهیم کرد که این سیاست دسترسی و نیازمندی مرتبط با آن با موفقیت پشت سر گذاشته شده‌است.

3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحله‌ی زیر است:
مراجعه‌ی به کلاس ImageGallery.WebApi.WebApp\Startup.cs و افزودن نیازمندی آن:
namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthorization(authorizationOptions =>
            {
                authorizationOptions.AddPolicy(
                   name: "MustOwnImage",
                   configurePolicy: policyBuilder =>
                    {
                        policyBuilder.RequireAuthenticatedUser();
                        policyBuilder.AddRequirements(new MustOwnImageRequirement());
                    });
            });
            services.AddScoped<IAuthorizationHandler, MustOwnImageHandler>();
ابتدا باید MustOwnImageHandler تهیه شده را به سیستم تزریق وابستگی‌ها معرفی کنیم.
سپس یک Policy جدید را با نام دلخواه MustOwnImage تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه می‌کنیم. همانطور که ملاحظه می‌کنید یک وهله‌ی جدید از MustOwnImageRequirement در اینجا ثبت شده‌است. همین وهله به متد HandleRequirementAsync نیز ارسال می‌شود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، می‌توان خواص مرتبطی را به کلاس MustOwnImageRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را می‌توان ثبت کرد و نه Handler آن‌را. این Handler از سیستم تزریق وابستگی‌ها بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا می‌شود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آن‌را به سیستم تزریق وابستگی‌ها معرفی کرده‌اید).

پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
[Authorize(Policy ="MustOwnImage")]
به صورت خودکار توسط MustOwnImageHandler مدیریت می‌شود.


اعمال سیاست دسترسی پویای تعریف شده به Web API

پس از تعریف سیاست دسترسی MustOwnImage که پویا عمل می‌کند، اکنون نوبت به استفاده‌ی از آن در کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs است:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet("{id}", Name = "GetImage")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> GetImage(Guid id)
        {
        }

        [HttpDelete("{id}")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> DeleteImage(Guid id)
        {
        }

        [HttpPut("{id}")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> UpdateImage(Guid id, [FromBody] ImageForUpdateModel imageForUpdate)
        {
        }
    }
}
در اینجا در سه قسمت GetImage ،DeleteImage و UpdateImage با اعمال سیاست دسترسی پویای MustOwnImage، اگر کاربر جاری همان Owner تصویر درخواستی نباشد، دسترسی او به اجرای کدهای داخل این اکشن متدها به صورت خودکار بسته خواهد شد.




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
پاسخ به بازخورد‌های پروژه‌ها
ادامه آموزش MVVM در صورت امکان با MVVM Light
بیش از 15 قسمت آموزش MVVM در سایت برای انجام یک پروژه واقعی کامل است (و من در کارهای خودم بدون مشکل از آن‌ها استفاده می‌کنم).
 در این آموزش‌ها به MVVM Light هم پرداخته شده، ضمن اینکه زیر ساخت آن و کتابخانه‌های مشابه نیز بررسی شدن.
مطالب
حرکت دادن و تغییر اندازه کنترل‌های فرم در زمان اجرا با استفاده از ماوس
برخی اوقات نیاز است در یک فرم ویندوزی، کنترل‌های آن‌را در حال اجرا با استفاده از ماوس جابجا کنیم و یا اندازه‌ی آن‌ها را تغییر بدیم.
در وب راهکار‌های مختلفی برای این کار ارائه شده، ولی این راه‌ها معمولا یا فقط برای تغییر مکان و یا فقط برای تغییر اندازه کنترل‌ها ارائه شده‌اند. من یکی از مقالات کد پروجکت را که به جابجا کردن کنترل‌ها پرداخته بود، توسعه دادم که امکان تغییر اندازه هم به آن اضافه شود. مقاله‌ی من (به زبان انگلیسی) در اینجا قرار دارد.
چون از کلاس و متد‌های استاتیک استفاده کردم، روش استفاده از این کلاس ساده بوده و افزودن قابلیت تغییر اندازه و جابجایی زمان اجرا با ماوس برای هر کنترل فقط با یک خط کد قابل انجام است:
ControlMoverOrResizer.Init(button1);
نحوه‌ی استفاده از کلاس:
برای فعال کردن قابلیت تغییر اندازه و جابجایی یک کنترل در حال اجرای برنامه با موس ما باید متد Init از کلاس MoveAndResizeControls  را فراخوانی کنیم و کنترل را به عنوان پارامتر به آن بفرستیم.
ControlMoverOrResizer.Init(button1);
اگر که ما بخواهیم به همراه تغییر کنترل ، خواص container آن را هم تغییر دهیم. باید کنترل container  را به عنوان پارامتر دوم به متد مذکور ارسال کنیم.
ControlMoverOrResizer.Init(button2,panel1);
برخی اوقات ممکن است که ما فقط بخواهیم که یا کنترل‌ها را جابجا کنیم و یا اندازه‌ی آنها را تغییر دهیم؛ در این مواقع ما باید خاصیت WorkType  کلاس MoveAndResizeControls را تغییر دهیم به یکی از مقادیر ذیل تغییر دهیم .
         internal enum MoveOrResize
        {
            Move,
            Resize,
            MoveAndResize
        }

مثالی از نحوه‌ی کار با کلاس :
using System;
using System.Windows.Forms;
using ControlManager;
namespace MoveAndResizeControls
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            ControlMoverOrResizer.Init(button1);
            ControlMoverOrResizer.Init(groupBox1);
            ControlMoverOrResizer.Init(textBox1);
            ControlMoverOrResizer.Init(button2,panel1);
            comboBox1.SelectedIndex = 0;
        }

        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            switch (comboBox1.SelectedIndex)
            {
                case 0:
                    ControlMoverOrResizer.WorkType=ControlMoverOrResizer.MoveOrResize.MoveAndResize;
                    break;
                case 1:
                    ControlMoverOrResizer.WorkType = ControlMoverOrResizer.MoveOrResize.Move;
                    break;
                case 2:
                    ControlMoverOrResizer.WorkType = ControlMoverOrResizer.MoveOrResize.Resize;
                    break;
            }
        }
    }
}

نکته :بعد از انجام تغییرات، جهت ذخیره وضعیت کنترل‌ها و بازیابی مجدد آنها می‌توان از متد‌های زیر استفاده کرد:
 GetSizeAndPositionOfControlsToString  , SetSizeAndPositionOfControlsFromString



شکل حالت اولیه:

شکل حالت نتیجه:


نظرات مطالب
بررسی ویجت Kendo UI File Upload
سلام و با تشکر؛ یک سوال:
آیا امکان دسترسی به لیست فایل‌ها در حالت عدم استفاده از batch وجود دارد؟ به عنوان مثال حالتی را در نظر بگیرید که کاربر قرار است یک فرم را تکمیل کرده در این فرم یکی از فیلدها تصاویر می‌باشد (تصاویر درون جدول دیگری ذخیره خواهند شد) یعنی اکشن متد به اینصورت باشد:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(MyModel model, IEnumerable<HttpPostedFileBase> files)
{
            // code
            return View();
}
در اینحالت مقدار دریافتی files نال خواهد بود.
به عنوان راه‌حل جایگزین از روش "فعال سازی ارسال batch " استفاده کردم یعنی به این صورت:
[HttpPost]
        public ActionResult Save(MyModel model, IEnumerable<HttpPostedFileBase> files)
        {
            
                var sModel = new MyViewModel
                {
                    MainPhoto = model.MainPhoto,
                    Phone = model.Phone,
                    Title = model.Title,
                    WebSiteUrl = model.WebSiteUrl,
    // ....
                };
                if (files != null)
                {
                    var images = files.Select(file => new Image
                    {
                        FileName = Helpers.Helper.UploadFile(file)
                    }).ToList();
                    sModel.Images = images;
                }
                _locationService.AddLocation(sModel);
                _uow.SaveAllChanges();
            
            return RedirectToAction("List");
        }

   حالت فوق به خوبی کار میکند اما مشکل آن عدم نمایش قسمت لیست فایل‌ها و همچنین قسمت درصد پیشرفت فایل است.