نظرات مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی
بهبود قسمت «ارسال کوئری استرینگ‌ها به کامپوننت‌های مختلف » در Blazor 6x

در Blazor 6x دیگر نیازی نیست تا با استفاده از «HttpUtility.ParseQueryString»، کار پردازش دستی کوئری استرینگ‌های رسیده، صورت گیرد. برای نمونه فرض کنید Url رسیده، چنین شکلی را دارد:
/search?filter=some+stuff&page=3&assignee=User1&assignee=User2
برای دسترسی به مقادیر کوئری استرینگ‌های آدرس فوق، اینبار کافی است به صورت زیر عمل کنیم:
@code {
    [Parameter]
    [SupplyParameterFromQuery]
    public string Filter { get; set; }

    [Parameter]
    [SupplyParameterFromQuery]
    public int? Page { get; set; }

    [Parameter]
    [SupplyParameterFromQuery(Name = "assignee")]
    public string[] Assignees { get; set; }
}
یعنی ذکر دو ویژگی [Parameter, SupplyParameterFromQuery] بر روی یک خاصیت عمومی کامپوننت، آن‌را آماده‌ی دریافت مقادیر کوئری استرینگ‌های متناظری می‌کند. اگر می‌خواهید نام پارامتر، با نام کوئری استرینگ یکی نباشد (حالت پیش‌فرض)، از خاصیت Name این ویژگی می‌توان استفاده کرد.

نکته: در اینجا مقادیری مانند ذیل قابل قبول هستند:
String, bool, DateTime, decimal, double, float, Guid, int, long
و یا حالت Nullable آن‌ها و یا آرایه‌ای آن‌ها.

امکان ساخت Urlهایی به همراه کوئری استرینگ‌ها جهت هدایت ساده‌تر به آن‌ها

به Blazor 6x، متدهای الحاقی UriWithQueryParameter نیز اضافه شده‌اند و کار آن‌ها، افزودن ساده‌تر کوئری‌استرینگ‌ها به Urlها است که نمونه‌ای از آن به صورت زیر است:
var actualUri = NavigationManager.GetUriWithQueryParameters(new Dictionary<string, object>
        {
            ["full name"] = "John Doe", // Single value
            ["ping"] = new int?[] { 35, 16, null, 87, 240 }
        });
که چنین Url ای را تولید می‌کند (نمونه‌ای از روش تعریف کوئری استرینگ‌های آرایه‌ای):
host/?full%20name=John%20Doe&ping=35&ping=16&ping=87&ping=240
مثال‌های بیشتر آن‌را در اینجا می‌توانید مشاهده کنید.
مطالب
آموزش Xamarin Forms - قسمت دوم - بررسی ساختار پروژه‌های زمارین

در  مقاله قبلی، درباره نحوه نصب و راه اندازی اولین پروژه Xamarin Forms کمی صحبت کردیم. حال وقت آن رسیده‌است که درباره ساختار اپلیکیشن‌های Xamarin Forms  بیشتر بحث کنیم. در سیستم عامل‌های مختلف، رابط‌های کاربری با اسامی مختلفی مانند Control ، Widget ، View  و Element صدا زده میشوند که هدف تمامی آنها نمایش و ارتباط با کاربر میباشد. در Xamarin Forms به تمام عناصری که در صفحه نمایش نشان داده میشوند، Visual Elements گفته میشود؛ که در سه گروه بندی اصلی قرار میگیرند:

· Page

· Layout

· View

هر چیزی که فضایی را در صفحه اشغال کند، یک Visual Element است. Xamarin Forms از یک ساختار سلسه مراتبی Parent-Child برای UI استفاده میکند. به طور مثال یک اپلیکیشن را در نظر بگیرید. هر اپلیکیشن به طور کلی از چندین صفحه تشکیل شده است. هر Page برای چینش کنترل‌های مختلف، از یک سری Layout استفاده میکند و هر Layout هم شامل چندین View مختلف میباشد.

 در  مقاله قبلی، پروژه اولیه خود را ساختیم. اگر به پروژه Shared مراجعه کنیم، خواهیم دید که این پروژه دارای کلاسی به نام App است. اگر به خاطر داشته باشید، گفتیم که این کلاس اولین Page درون اپلیکیشن را مشخص میکند و همچنین میتوان برای مدیریت LifeCycle اپلیکیشن مانند OnStart و ... از آن استفاده کرد. در متد سازنده، صفحه‌ای به نام MainPage به عنوان اولین صفحه برنامه مشخص شده بود. به کدهای این صفحه بار دیگر نگاهی کنیم تا بتوانیم کمی بر روی این کدها توضیحاتی را ارائه دهیم:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:PreviewerTest"
             x:Class="PreviewerTest.MainPage">
<Label Text="Welcome to Xamarin Forms!"
           VerticalOptions="Center"
           HorizontalOptions="Center" />
</ContentPage>

همانطور که میبینید ساختار سلسه مراتبی را به خوبی میتوانید در این کدها مشاهده کنید. در وهله اول یک ContentPage را به عنوان والد اصلی مشاهده میکنید. Page ‌ها در Xamarin Forms انواع مختلفی دارند که ContentPage یکی از آنهاست و از آن میتوانید به عنوان یک صفحه ساده استفاده کنید.

در درون این صفحه یک Label را به عنوان Child صفحه مشاهده میکنید (تمامی کنترل‌ها در زمارین در زیر گروه View قرار میگیرند).

نتیجه این کدها صفحه‌ای ساده با یک لیبل است که تمامی صفحه را اشغال کرده‌است. اگر شما View دیگری را در زیر این لیبل اضافه کنید خواهید دید که این دو، روی هم می‌افتند و شما نمیتوانید کنترل زیر آن را مشاهده کنید. همانطور که در بالا گفتیم زمارین از المنتی به نام Layout برای چینش عناصر استفاده میکند. Layout ‌های مختلفی در زمارین وجود دارند که هر کدام به طُرق مختلفی این عناصر را در کنار هم میچینند. یکی از آنها StackLayout میباشد. 

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamarinSample"
             x:Class="XamarinSample.MainPage">
    <StackLayout>
        <Label Text="Welcome to Xamarin Forms!" VerticalOptions="Center" HorizontalOptions="Center" />
        <Button Text="Ok!"/>
    </StackLayout>
</ContentPage>

StackLayout عناصر فرزند خود را به صورت افقی و عمودی در کنار هم در صفحه میچیند. 

اگر به خاطر داشته باشید، در هنگام ساخت پروژه زمارین چندین پروژه برای پلتفرم‌های مختلف در کنار آن ساخته شد. پروژه XamarinSample.Android  برای ساخت و مدیریت پروژه در پلتفرم اندروید، مورد استفاده قرار میگیرد. همانطور که گفتیم کدهای درون این پروژه‌ها با پروژه Shared ادغام شده و با هم اجرا خواهند شد. وقت آن رسیده که سری به کدهای آن بزنیم و نحوه‌ی اجرای پروژه Shared را توسط پروژه اندروید ببینیم.

وقتی پروژه اندروید را باز کنید با کلاسی به نام MainActivity مواجه خواهید شد. این کلاس وظیفه ایجاد Activity اصلی برنامه را دارد. 

namespace XamarinSample.Droid {
 [Activity(Label = "XamarinSample", Icon = "@drawable/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
 public class MainActivity: global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity {
  protected override void OnCreate(Bundle bundle) {
   TabLayoutResource = Resource.Layout.Tabbar;
   ToolbarResource = Resource.Layout.Toolbar;

   base.OnCreate(bundle);

   global::Xamarin.Forms.Forms.Init(this, bundle);
   LoadApplication(new XamarinSample.App());
  }
 }
}

در Attribute بالای سر کلاس، برخی از ویژگی‌ها مانند تم، آیکن، سایز و ... مقداردهی شده‌اند. همچنین باعث میشود که در صورت تغییر Orientation و سایز، Activity از اول ساخته نشود. در متد OnCreate علاوه بر استایل دهی به TabLayout و ToolBar ‌ها متدی به نام Forms.Init صدا زده شده است. این متد استاتیک که در تمامی پروژه‌ها فراخوانی میشود، سیستم Xamarin Forms را در هر کدام از پلت فرم‌ها بارگزاری میکند.

مطالب
کتابخانه GMap.Net
نقشه گوگل در حال حاضر یکی از محبوب‌ترین و کاملترین نقشه‌های جهان است و امکانات خوبی هم دارد. در این راستا بسیاری از مردم سعی در استفاده از این نقشه‌ها و امکانات آن‌ها دارند. به همین دلیل گوگل در بسته‌های api خود نیز این مورد را گنجانده است. ولی استفاده از این api مستلزم نوشتن کدهای جاوا اسکرپیتی و شناخت توابع و ثابت‌های api گوگل است. اما در هر صورت این مستندات مورد مطالعه قرار می‌گیرند.

سال گذشته بود که به بررسی کتابخانه‌های موجود برای دات نت که به ساخت نقشه‌های گوگل (+ ) می‌پردازند پرداختم. ولی مشکلی که وجود داشت، همه آن‌ها در نهایت یک تصویر jpeg تحویل می‌دادند. ولی من می‌خواستم نقشه‌ی من زنده و واکنشگرا باشد تا کاربر بتواند روی آن حرکت کند، زوم کند، مارکر‌ها را جابجا کند و امکانات دیگری که در این نقشه در دسترس است را داشته باشد. برای همین شروع به ساخت یک class library کردم تا کاربر بتواند در محیط سی شارپ، تنظیمات را با اسامی قابل شناخت و یک intellisense قدرتمند بنویسد و در نهایت بر اساس اطلاعات کاربر، این کدها به صورت جاوا اسکریپت تولید شود. می‌توانید سورس نهایی کتابخانه‌ی GMap.Net را در گیت هاب، به همراه یک پروژه‌ی نمونه ببینید.

پروژه‌ی وابسته این کتابخانه،  MS Ajax Minifier جهت کم حجم کردن کدهای جاوا اسکریپت است. در مورد این کتابخانه در سایت جاری بحث شده است.
برای نصب این کتابخانه می‌توانید از طریق دستور زیر در Nuget عمل کنید:
Install-Package GMap.Net

در این کتابخانه مواردی که مورد توجه قرار گرفته است، تنظیمات نقشه و بعد از آن overlay‌ها هستند که شامل مارکرها و اشکال مختلف می‌باشند. این اشکال شامل رسم مستطیل بر روی نقشه، چند ضلعی‌ها و ... نیز می‌شوند.

برای شروع نیاز است که یک نمونه از کلاس GoogleMapApi را ایجاد کنید. بعد از آن با استفاده از خصوصیت SetLocation، مختصات مرکز نقشه را تنظیم نمایید. سپس با استفاده از خصوصیات دیگر نیز می‌توانید نقشه را تنظیم نمایید. تعدادی از این خصوصیات مثل SetZoomVisibility هستند که با استفاده از آن می‌توانید تنظیمات زوم را روی نقشه پیاده سازی کنید. البته فعال کردن این گزینه به تنهایی کافی نیست و باید از طریق خصوصیت ZoomControlOption پیکربندی کنترل زوم را نیز اینجام دهید که این پیکربندی شامل موقعیت قرارگیری کنترل‌های زوم و اندازه‌ی دکمه‌ها می‌باشد و مابقی تنظیمات هم بدین شکل هستند:

به غیر از تنظیمات نقشه، Overlayهای زیر در این کلاس پشتیبانی می‌شوند:
عنوان
توضیحات
 Marker  یک نشانه گذار که برای مشخص کردن یک محل بر روی نقشه به کار می‌رود. این علامت گذار شامل خصوصیت‌هایی چون نقطه‌ی قرارگیری، آیکن، عنوان و انیمیشنی برای نحوه‌ی نمایش آن می‌باشد. همچنین شامل یک خصوصیت دیگر از نوع InfoWindow است که به شما امکان نمایش یک پنجره‌ی توضیحات را نیز بر روی مارکر می‌دهد. این محتوا می‌تواند به صورت HTML نمایش یابد.
 Circle  در صورتیکه بخواهید ناحیه‌ای دایره‌ای شکل را بر روی نقشه مشخص کنید، کاربرد دارد. با دادن نقطه‌ی مرکزی و شعاع می‌توانید دایره را ترسیم کنید. همچنین شامل خصوصیات ظاهری چون رنگ داخل و حاشیه‌ها و میزان شفافیت نیز می‌باشد.
 Rectangle  به رسم یک مستطیل می‌پردازد و تنها لازم است دو مختصات را به آن بدهید و بر اساس این دو نقطه، ناحیه‌ی مستطیلی شکل ترسیم می‌گردد. در صورتیکه نقاط بیشتری را به آن اضافه کنید، فقط دوتای اولی در نظر گرفته می‌شوند. این گزینه شامل خصوصیات ظاهری نیز می‌گردد.
 Polyline  برای ترسیم مسیرها به صورت چند ضلعی به کار می‌رود و الزامی به بستن مسیرها نیست. دارای خصوصیات ظاهری نیز می‌باشد.
 
polygon
کاملا شبیه Ployline است؛ با این تفاوت که یک چند ضلعی بسته است و می‌تواند داخل آن با رنگ پر باشد. برای بستن این چند ضلعی لازم نیست تا کاری انجام دهید. خود کلاس، نقطه‌ی اول و آخر را به هم وصل می‌کند.

خصوصیات آیتم‌های بالا، شامل موارد زیر می‌شود:

 نام خصوصیت
توضیحات
 Id  در سازنده‌ی هر کدام به طور اجباری قرار گرفته است. این id برای زمانی است که بخواهید با استفاده از جاوااسکرپیت با آن ارتباط برقرار کنید.
 Editable  با فعال کردن این خاصیت، به کاربر این اجازه را می‌دهید که بتواند روی Overlay ویرایش انجام دهد.
 StrokeWeight  ضخامت لبه‌ها را مشخص می‌کند.
 StrokeColor  رنگ لبه را مشخص می‌کند.
 StrokeOpacity  میزان شفافیت لبه را بین 0 تا 1 مشخص می‌کند.
 FillColor  بعضی از المان‌ها مانند چند ضلعی‌های بسته و مستطیل که ناحیه‌ی داخلی دارند، شامل این گزینه هستند و رنگ داخل این ناحیه را مشخص می‌کنند.
 FillOpacity  میزان شفافیت خصوصیت بالا را از 0 تا 1 مشخص می‌کند.
 Points  با استفاده از این خاصیت می‌توانید مختصات را با استفاده از کلاس Location به آن اضافه کنید. برای دایره خصوصیت Point وجود دارد.
 Radius  برای دایره کاربرد دارد. با مقدار نوع Int می‌توانید شعاع آن را مقدار دهی کنید.
کد زیر و تصویر زیر نمونه‌ای از کاربرد این کلاس است:
public class MiladTower
    {
        public GoogleMapApi TestMarker()
        {
            var map=new GoogleMapApi(true);
            var location = new Location(35.7448416, 51.3753212);
            map.SetLocation(location);
            map.SetZoom(17);
            map.SetMapType(MapTypes.ROADMAP);
            map.SetBackgroundColor(Color.Aqua);
            map.ZoomControlVisibilty(true);
            map.ZoomOptions = new zoomControlOptions()
            {
                Position = Position.TOP_LEFT,
                ZoomStyle = ZoomStyle.SMALL
            };
  

            Marker marker=new Marker("mymarker1");
            marker.InfoWindow=new InfoWindow("iw1")
            {
                Content = "<b>Milad Tower</b><i>in Tehran</i><br/>Milad Tower is the highest tower in iran,many people and tourists visit it each year, but it's so expensive that i cant afford it as iranian citizen<br/>please see more info at  <a href=\"https://en.wikipedia.org/wiki/Milad_Tower\"><img width='16px' height='16px' src='https://en.wikipedia.org/favicon.ico'/>wikipedia</a>"
            };
            marker.MarkerPoint = location;
            map.Markers.Add(marker);

            var circle=new CircleMarker("mymarker2");
            circle.FillColor = Color.Green;
            circle.FillOpacity = 0.6f;
            circle.StrokeColor = Color.Red;
            circle.StrokeOpacity = 0.8f;
            circle.Point = location;
            circle.Radius = 30;
            circle.Editable = true;
            circle.StrokeWeight = 3;
            map.Circles.Add(circle);

            Rectangle rect=new Rectangle("rect1");
            rect.FillColor = Color.Black;
            rect.FillOpacity = 0.4f;
            rect.Points.Add(new Location(35.74728723483808, 51.37550354003906));
            rect.Points.Add(new Location(35.74668641224311, 51.376715898513794));
            map.Rectangles.Add(rect);

            Polyline polyline=new Polyline("poly1");
            polyline.Points.Add(new Location(35.74457043569041, 51.373915672302246));
            polyline.Points.Add(new Location(35.74470976097927, 51.37359380722046));
            polyline.Points.Add(new Location(35.744378863020074, 51.37337923049927));
            polyline.StrokeColor = Color.Blue;
            polyline.StrokeWeight = 2;
            map.Polylines.Add(polyline);

            Polygon polygon=new Polygon("poly2");
            polygon.Points.Add(new Location(35.746494844665094, 51.374655961990356));
            polygon.Points.Add(new Location(35.74635552250061, 51.37283205986023));
            polygon.Points.Add(new Location(35.74598109297522, 51.372681856155396));
            polygon.Points.Add(new Location(35.7454934611854, 51.37361526489258));
            polygon.FillColor = Color.Black;
            polygon.FillOpacity = 0.5f;
            polygon.StrokeColor = Color.Gray;
            polygon.StrokeWeight = 1;
            map.Polygons.Add(polygon);

            return map;
        }
    }
بعد از ایجاد چنین کلاسی نیاز است تا آن را در ویوو ترسیم کنیم. ابتدا یک div برای ناحیه‌ی نقشه ایجاد می‌کنیم و سپس خروجی متد ShowMapForMVC را در ناحیه‌ی Head صفحه چاپ می‌کنیم. این خروجی به طور خودکار یک MVCHTMLString را بر می‌گرداند. در صورتیکه از وب فرم استفاده می‌کنید، می‌توانید از گزینه‌ی ShowMap استفاده کنید.
@section javascript
{
    @{
        var map = new MiladTower().TestMarker();
        @map.ShowMapForMvc("mapdiv")
    }
}
<br/><br/>
<div id="mapdiv" style="width:600px;height:600px;"></div>

در نهایت نقشه‌ی زیر نمایش داده می‌شود:


کم حجم کردن کدها
در صورتیکه به سورس صفحه نگاهی بیندازید، می‌بینید که کدهای جاوا اسکریپت، داخل صفحه نوشته شده‌اند. اگر بخواهید برای کم حجم‌تر شدن کد، عملیات minify را انجام دهید، با true شدن خصوصیت minified با استفاده از کتابخانه‌ی وابسته‌اش (MS Ajax Minifier)  اینکار را انجام می‌دهد.

انتقال کدها به یک فایل خارجی
بسیاری از ما برای نوشتن کدهای جاوا اسکریپت، از یک فایل خارجی استفاده می‌کنیم. برای داشتن این قابلیت می‌توانید به جای ShowMapForMVC متد CallJs را صدا بزنید تا کتابخانه api گوگل را صدا بزند و سپس در یک اکشن متد، متد GiveJustJS را صدا بزنید و طبق مقاله‌ی موجود در سایت جاری محتوای آن را برگردانید و به عنوان یک فایل JS به این اکشن متد لینک بدهید. کدهای زیر به شما نحوه‌ی این کار را نشان می‌دهند:
ابتدا در یک اکشن متد، کد زیر را وارد می‌کنیم:
    public ActionResult MiladJs()
        {
            var output = new MiladTower().TestMarker().GiveJustJs("mapdiv");
            Response.ContentType = "text/javascript";
            return Content(output);
        }

بعد از آن در ویووی مربوطه کد زیر را داریم:
@section javascript
{
    @{
        var map = new MiladTower().TestMarker();
        @map.CallJs()
        <script type="text/javascript" src="@Url.Action("MiladJs","Home")"></script>
    }   
}
<br/><br/>
<div id="mapdiv" style="width:600px;height:600px;"></div>
بدین ترتیب کدهای شما داخل یک فایل خارجی قرار می‌گیرند.


نحوه‌ی کارکرد این کتابخانه
برای آشنایی با نحوه‌ی کارکرد آن باید بدانید که اساس کار این کتابخانه string interpolation است. این کتابخانه کلاسی را به صورت Partial دارد که بین چندین فایل تقسیم شده است و هر یک از فایل‌ها، با نام محتوای آن نامگذاری شده‌اند. Public methods متدهای عمومی، private methods متدهای خصوصی، Constants یا ثابت‌ها که حاوی تمام دستورات جاوا اسکریپتی هستند و در نهایت خود کلاس اصلی GoogleMapApi که حاوی کدهای اجرایی و تشکیل کد جاوا اسکریپت می‌باشد. در کنار کلاس اصلی، کلاس‌های Overlay هم قرار دارند که شامل اطلاعات اشیاء روی نقشه‌ها هستند؛ مثل مارکرها و چندضلعی‌ها و ... و در نهایت یک سری کلاس و نوع شمارشی Enum شامل خصوصیت‌هایی که برای تنظیمات و پیکربندی نقشه به کار می‌روند.
کلاس GoogleMapApi برای ایجاد کدها، داده‌هایی را که برای هر کلاس در نظر گرفته‌اید، با استفاده از interpolation و ثابت‌های حاوی کد جاوا اسکریپت، در یک رشته‌ی جدید قرار می‌دهند و این رشته‌ها با اتصال درست در موقعیت خود، کد نهایی جاوا اسکریپت را تولید می‌کنند.
مطالب
React 16x - قسمت 28 - احراز هویت و اعتبارسنجی کاربران - بخش 3 - فراخوانی منابع محافظت شده و مخفی کردن عناصر صفحه
ارسال خودکار هدرهای ویژه‌ی Authorization، به سمت سرور

در برنامه‌ی backend این سری (که از انتهای مطلب قابل دریافت است)، به Controllers\MoviesController.cs مراجعه کرده و متدهای Get/Delete/Create آن‌را با فیلتر [Authorize] مزین می‌کنیم تا دسترسی به آن‌ها، تنها به کاربران لاگین شده‌ی در سیستم، محدود شود. در این حالت اگر به برنامه‌ی React مراجعه کرده و برای مثال سعی در ویرایش رکوردی کنیم، اتفاقی رخ نخواهد داد:


علت را نیز در برگه‌ی network کنسول توسعه دهندگان مرورگر، می‌توان مشاهده کرد. این درخواست از سمت سرور با Status Code: 401، برگشت خورده‌است. برای رفع این مشکل باید JSON web token ای را که در حین لاگین، از سمت سرور دریافت کرده بودیم، به همراه درخواست خود، مجددا به سمت سرور ارسال کنیم. این ارسال نیز باید به صورت یک هدر مخصوص با کلید Authorization و مقدار "Bearer jwt" باشد.
به همین جهت ابتدا به src\services\authService.js مراجعه کرده و متدی را برای بازگشت JWT ذخیره شده‌ی در local storage به آن اضافه می‌کنیم:
export function getLocalJwt(){
  return localStorage.getItem(tokenKey);
}
سپس به src\services\httpService.js مراجعه کرده و از آن استفاده می‌کنیم:
import * as auth from "./authService";

axios.defaults.headers.common["Authorization"] = "Bearer " + auth.getLocalJwt();
کار این یک سطر که در ابتدای ماژول httpService قرار می‌گیرد، تنظیم هدرهای پیش‌فرض تمام انواع درخواست‌های ارسالی توسط Axios است. البته می‌توان از حالت‌های اختصاصی‌تری نیز مانند فقط post، بجای common استفاده کرد. برای نمونه در تنظیم فوق، تمام درخواست‌های HTTP Get/Post/Delete/Put ارسالی توسط Axios، دارای هدر Authorization که مقدار آن به ثابتی شروع شده‌ی با Bearer و سپس مقدار JWT دریافتی از سرور تنظیم می‌شود، خواهند بود.

مشکل! اگر برنامه را در این حالت اجرا کنید، یک چنین خطایی را مشاهده خواهید کرد:
Uncaught ReferenceError: Cannot access 'tokenKey' before initialization
علت اینجا است که سرویس httpService، دارای ارجاعی به سرویس authService شده‌است و برعکس (در httpService، یک import از authService را داریم و در authService، یک import از httpService را)! یعنی یک وابستگی حلقوی و دو طرفه رخ‌داده‌است.
برای رفع این خطا باید ابتدا مشخص کنیم که کدامیک از این ماژول‌ها، اصلی است و کدامیک باید وابسته‌ی به دیگری باشد. در این حالت httpService، ماژول اصلی است و بدون آن و با نبود امکان اتصال به backend، دیگر authService قابل استفاده نخواهد بود.
به همین جهت به httpService مراجعه کرده و import مربوط به authService را از آن حذف می‌کنیم. سپس در همینجا متدی را برای تنظیم هدر Authorizationاضافه کرده و آن‌را به لیست default exports این ماژول نیز اضافه می‌کنیم:
function setJwt(jwt) {
  axios.defaults.headers.common["Authorization"] = "Bearer " + jwt;
}

//...

export default {
  // ...
  setJwt
};
در آخر، در authService که ارجاعی را به httpService دارد، فراخوانی متد setJwt را در ابتدای ماژول، انجام خواهیم داد:
http.setJwt(getLocalJwt());
به این ترتیب، وابستگی حلقوی بین این دو ماژول برطرف می‌شود و اکنون این authService است که به httpService وابسته‌است و نه برعکس.

تا اینجا اگر تغییرات را ذخیره کرده و سعی در ویرایش یکی از رکوردهای فیلم‌های نمایش داده شده کنیم، این‌کار با موفقیت انجام می‌شود؛ چون اینبار درخواست ارسالی، دارای هدر ویژه‌ی authorization است:



روش بررسی انقضای توکن‌ها در سمت کلاینت

اگر JWT قدیمی و منقضی شده‌ی از روز گذشته را آزمایش کنید، باز هم از سمت سرور، Status Code: 401 دریافت خواهد شد. اما اینبار در لاگ‌های برنامه‌ی سمت سرور، OnChallenge error مشخص است. در این حالت باید یکبار logout کرد تا JWT قدیمی حذف شود. سپس نیاز به لاگین مجدد است تا یک JWT جدید دریافت گردد. می‌توان اینکار را پیش از ارسال اطلاعات به سمت سرور، در سمت کلاینت نیز بررسی کرد:
function checkExpirationDate(user) {
  if (!user || !user.exp) {
    throw new Error("This access token doesn't have an expiration date!");
  }

  user.expirationDateUtc = new Date(0); // The 0 sets the date to the epoch
  user.expirationDateUtc.setUTCSeconds(user.exp);

  const isAccessTokenTokenExpired =
    user.expirationDateUtc.valueOf() < new Date().valueOf();
  if (isAccessTokenTokenExpired) {
    throw new Error("This access token is expired!");
  }
}
در اینجا user، همان شیء حاصل از const user = jwtDecode(jwt) است که در قسمت قبل به آن پرداختیم. سپس خاصیت exp آن با زمان جاری مقایسه شده و در صورت وجود مشکلی، استثنایی را صادر می‌کند. می‌توان این متد را پس از فراخوانی jwtDecode، قرار داد.


محدود کردن حذف رکوردهای فیلم‌ها به نقش Admin در Backend

تا اینجا تمام کاربران وارد شده‌ی به سیستم، می‌توانند علاوه بر ویرایش فیلم‌ها، آن‌ها را نیز حذف کنند. به همین جهت می‌خواهیم دسترسی حذف را از کاربرانی که ادمین نیستند، بگیریم. برای این منظور، در سمت سرور کافی است در کنترلر MoviesController، ویژگی [Authorize(Policy = CustomRoles.Admin)] را به اکشن متد Delete، اضافه کنیم. به این ترتیب اگر کاربری در سیستم ادمین نبود و درخواست حذف رکوردی را صادر کرد، خطای 403 را از سمت سرور دریافت می‌کند:


در برنامه‌ی مثال backend این سری، در فایل Services\UsersDataSource.cs، یک کاربر ادمین پیش‌فرض ثبت شده‌است. مابقی کاربرانی که به صورت معمولی در سایت ثبت نام می‌کنند، ادمین نیستند.
در این حالت اگر کاربری ادمین بود، چون در توکن او که در فایل Services\TokenFactoryService.cs صادر می‌شود، یک User Claim ویژه‌ی از نوع Role و با مقدار Admin وجود دارد:
if (user.IsAdmin)
{
   claims.Add(new Claim(ClaimTypes.Role, CustomRoles.Admin, ClaimValueTypes.String, _configuration.Value.Issuer));
}
این مقدار، در payload توکن نهایی او نیز ظاهر خواهد شد:
{
  // ...
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  // ...
}
بنابراین هربار که برنامه‌ی React ما، هدر Bearer jwt را به سمت سرور ارسال می‌کند، فیلتر Authorize محدود شده‌ی به نقش ادمین، این نقش را در صورت وجود در توکن او، پردازش کرده و دسترسی‌های لازم را به صورت خودکار صادر می‌کند و همانطور که پیشتر نیز عنوان شد، اگر کاربری این نقش را به صورت دستی به توکن ارسالی به سمت سرور اضافه کند، به دلیل دسترسی نداشتن به کلیدهای خصوصی تولید مجدد امضای دیجیتال توکن، درخواست او در سمت سرور تعیین اعتبار نشده و برگشت خواهد خورد.


نکته 1: اگر در اینجا چندین بار یک User Claim را با مقادیر متفاوتی، به لیست claims اضافه کنیم، مقادیر آن در خروجی نهایی، به شکل یک آرایه ظاهر می‌گردند.

نکته 2: پیاده سازی سمت سرور backend این سری، یک باگ امنیتی مهم را دارد! در حین ثبت نام، کاربران می‌توانند مقدار خاصیت isAdmin شیء User را:
    public class User : BaseModel
    {
        [Required, MinLength(2), MaxLength(50)]
        public string Name { set; get; }

        [Required, MinLength(5), MaxLength(255)]
        public string Email { set; get; }

        [Required, MinLength(5), MaxLength(1024)]
        public string Password { set; get; }

        public bool IsAdmin { set; get; }
    }
خودشان دستی تنظیم کرده و ارسال کنند تا به صورت ادمین ثبت شوند! به این مشکل مهم، اصطلاحا mass assignment گفته می‌شود.
راه حل اصولی مقابله‌ی با آن، داشتن یک DTO و یا ViewModel خاص قسمت ثبت نام و جدا کردن مدل متناظر با موجودیت User، از شیءای است که اطلاعات نهایی را از کاربر، دریافت می‌کند. شیءای که اطلاعات را از کاربر دریافت می‌کند، نباید دارای خاصیت isAdmin قابل تنظیم در حین ثبت نام معمولی کاربران سایت باشد. یک روش دیگر حل این مشکل، استفاده از ویژگی Bind و ذکر صریح نام خواصی است که قرار است bind شوند و نه هیچ خاصیت دیگری از شیء User:
[HttpPost]
public ActionResult<User> Create(
            [FromBody]
            [Bind(nameof(Models.User.Name), nameof(Models.User.Email), nameof(Models.User.Password))]
            User data)
        {
و یا حتی می‌توان ویژگی [BindNever] را بر روی خاصیت IsAdmin، در مدل User قرار داد.

نکته 3: اگر می‌خواهید در برنامه‌ی React، با مواجه شدن با خطای 403 از سمت سرور، کاربر را به یک صفحه‌ی عمومی «دسترسی ندارید» هدایت کنید، می‌توانید از interceptor سراسری که در قسمت 24 تعریف کردیم، استفاده کنید. در اینجا status code = 403 را جهت history.push به یک آدرس access-denied سفارشی و جدید، پردازش کنید.


نمایش یا مخفی کردن المان‌ها بر اساس سطوح دسترسی کاربر وارد شده‌ی به سیستم

می‌خواهیم در صفحه‌ی نمایش لیست فیلم‌ها، دکمه‌ی new movie را که بالای صفحه قرار دارد، به کاربرانی که لاگین نکرده‌اند، نمایش ندهیم. همچنین نمی‌خواهیم اینگونه کاربران، بتوانند فیلمی را ویرایش و یا حذف کنند؛ یعنی لینک به صفحه‌ی جزئیات ویرایشی فیلم‌ها و ستونی که دکمه‌ها‌ی حذف هر ردیف را نمایش می‌دهد، به کاربران وارد نشده‌ی به سیستم نمایش داده نشوند.

در قسمت قبل، در فایل app.js، شیء currentUser را به state اضافه کردیم و با استفاده از ارسال آن به کامپوننت NavBar:
<NavBar user={this.state.currentUser} />
 نام کاربر وارد شده‌ی به سیستم را نمایش دادیم. با استفاده از همین روش می‌توان شیء currentUser را به کامپوننت Movies ارسال کرد و سپس بر اساس محتوای آن، قسمت‌های مختلف صفحه را مخفی کرد و یا نمایش داد. البته در اینجا (در فایل app.js) خود کامپوننت Movies درج نشده‌است؛ بلکه مسیریابی آن‌را تعریف کرده‌ایم که با روش ارسال پارامتر به یک مسیریابی، در قسمت 15، قابل تغییر و پیاده سازی است:
<Route
   path="/movies"
   render={props => <Movies {...props} user={this.state.currentUser} />}
/>
در اینجا برای ارسال props به یک کامپوننت، نیاز است از ویژگی render استفاده شود. سپس پارامتر arrow function را به همان props تنظیم می‌کنیم. همچنین با استفاده از spread operator، این props را در المان JSX تعریف شده، گسترده و تزریق می‌کنیم؛ تا از سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، محروم نشویم و آن‌ها را از دست ندهیم. در نهایت المان کامپوننت مدنظر را همانند روش متداولی که برای تعریف تمام کامپوننت‌های React و تنظیم ویژگی‌های آن‌ها استفاده می‌شود، بازگشت می‌دهیم.

پس از این تغییر به فایل src\components\movies.jsx مراجعه کرده و شیء user را در متد رندر، دریافت می‌کنیم:
class Movies extends Component {
  // ...

  render() {
    const { user } = this.props;
    // ...
اکنون که در کامپوننت Movies به این شیء user دسترسی پیدا کردیم، توسط آن می‌توان قسمت‌های مختلف صفحه را مخفی کرد و یا نمایش داد:
{user && (
  <Link
    to="/movies/new"
    className="btn btn-primary"
    style={{ marginBottom: 20 }}
  >
    New Movie
  </Link>
)}
با این تغییر، اگر شیء user مقدار دهی شده باشد، عبارت پس از &&، در صفحه درج خواهد شد و برعکس:


در این تصویر همانطور که مشخص است، کاربر هنوز به سیستم وارد نشده‌است؛ بنابراین به علت null بودن شیء user، دکمه‌ی New Movie را مشاهده نمی‌کند.


روش دریافت نقش‌های کاربر وارد شده‌ی به سیستم در سمت کلاینت

همانطور که پیشتر در مطلب جاری عنوان شد، نقش‌های دریافتی از سرور، یک چنین شکلی را در jwtDecode نهایی (یا user در اینجا) دارند:
{
  // ...
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  // ...
}
که البته اگر چندین Role تعریف شده باشند، مقادیر آن‌ها در خروجی نهایی، به شکل یک آرایه ظاهر می‌گردد. بنابراین برای بررسی آن‌ها می‌توان نوشت:
function addRoles(user) {
  const roles =
    user["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
  if (roles) {
    if (Array.isArray(roles)) {
      user.roles = roles.map(role => role.toLowerCase());
    } else {
      user.roles = [roles.toLowerCase()];
    }
  }
}
کار این متد، دریافت نقش و یا نقش‌های ممکن از jwtDecode، و بازگشت آن‌ها (افزودن آن‌ها به صورت یک خاصیت جدید، به نام roles، به شیء user دریافتی) به صورت یک آرایه‌ی با عناصری LowerCase است. سپس اگر نیاز به بررسی نقش‌، یا نقش‌های کاربری خاص بود، می‌توان از یکی از متدهای زیر استفاده کرد:
export function isAuthUserInRoles(user, requiredRoles) {
  if (!user || !user.roles) {
    return false;
  }

  if (user.roles.indexOf(adminRoleName.toLowerCase()) >= 0) {
    return true; // The `Admin` role has full access to every pages.
  }

  return requiredRoles.some(requiredRole => {
    if (user.roles) {
      return user.roles.indexOf(requiredRole.toLowerCase()) >= 0;
    } else {
      return false;
    }
  });
}

export function isAuthUserInRole(user, requiredRole) {
  return isAuthUserInRoles(user, [requiredRole]);
}
متد isAuthUserInRoles، آرایه‌ای از نقش‌ها را دریافت می‌کند و سپس بررسی می‌کند که آیا کاربر انتخابی، دارای این نقش‌ها هست یا خیر و متد isAuthUserInRole، تنها یک نقش را بررسی می‌کند.
در این کدها، adminRoleName به صورت زیر تامین شده‌است:
import { adminRoleName, apiUrl } from "../config.json";
یعنی محتویات فایل config.json تعریف شده را به صورت زیر با افزودن نام نقش ادمین، تغییر داده‌ایم:
{
  "apiUrl": "https://localhost:5001/api",
  "adminRoleName": "Admin"
}


عدم نمایش ستون Delete ردیف‌های لیست فیلم‌ها، به کاربرانی که Admin نیستند

اکنون که امکان بررسی نقش‌های کاربر لاگین شده‌ی به سیستم را داریم، می‌خواهیم ستون Delete ردیف‌های لیست فیلم‌ها را فقط به کاربری که دارای نقش Admin است، نمایش دهیم. برای اینکار نیاز به دریافت شیء user، در src\components\moviesTable.jsx وجود دارد. یک روش دریافت کاربر جاری وارد شده‌ی به سیستم، همانی است که تا به اینجا بررسی کردیم: شیء currentUser را به صورت props، از بالاترین کامپوننت، به پایین‌تر کامپوننت موجود در component tree ارسال می‌کنیم. روش دیگر اینکار، دریافت مستقیم کاربر جاری از خود src\services\authService.js است و ... اینکار ساده‌تر است! به علاوه اینکه همیشه بررسی تاریخ انقضای توکن را نیز به صورت خودکار انجام می‌دهد و در صورت انقضای توکن، کاربر را در قسمت catch متد getCurrentUser، از سیستم خارج خواهد کرد.
بنابراین در src\components\moviesTable.jsx، ابتدا authService را import می‌کنیم:
import * as auth from "../services/authService";
در ادامه ابتدا تعریف ستون حذف را از آرایه‌ی columns خارج کرده و تبدیل به یک خاصیت می‌کنیم. یعنی در ابتدای کار، چنین ستونی تعریف نشده‌است:
class MoviesTable extends Component {
  columns = [ ... ];
  // ...

  deleteColumn = {
    key: "delete",
    content: movie => (
      <button
        onClick={() => this.props.onDelete(movie)}
        className="btn btn-danger btn-sm"
      >
        Delete
      </button>
    )
  };
در آخر در متد سازنده‌ی این کامپوننت، کاربر جاری را از authService دریافت کرده و اگر این کاربر دارای نقش Admin بود، ستون deleteColumn را به لیست ستون‌های موجود، اضافه می‌کنیم تا نمایش داده شود:
  constructor() {
    super();
    const user = auth.getCurrentUser();
    if (user && auth.isAuthUserInRole(user, "Admin")) {
      this.columns.push(this.deleteColumn);
    }
  }
اکنون برای آزمایش برنامه، یکبار از آن خارج شوید؛ دیگر نباید ستون Delete نمایش داده شود. همچنین یکبار هم تحت عنوان یک کاربر معمولی در سایت ثبت نام کنید. این کاربر نیز چنین ستونی را مشاهده نمی‌کند.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-28-backend.zip و sample-28-frontend.zip
مطالب
اضافه کردن Watermark به تصاویر یک برنامه ASP.NET MVC در صورت لینک شدن در سایتی دیگر
درگیر شدن با سایت‌های دیگر که چرا مطالب ما را کپی کرده‌اید نهایتا بجز فرسایش عصبی حاصل دیگری را به همراه ندارد. اساسا زمانیکه مطلبی را به صورت باز در اینترنت انتشار می‌دهید، قید کپی شدن یا نشدن آن‌را باید زد. اما ... می‌توان همین سایت‌ها را تبدیل به تبلیغ کننده‌های رایگان کار خود نمود که در ادامه نحوه انجام آن‌را در یک برنامه ASP.NET MVC بررسی خواهیم کرد:

الف) نیاز است ارائه تصاویر تحت کنترل برنامه باشند.

using System.IO;
using System.Net.Mime;
using System.Web.Mvc;

namespace MvcWatermark.Controllers
{
    public class HomeController : Controller
    {
        const int ADay = 86400;

        public ActionResult Index()
        {
            return View();
        }

        [OutputCache(VaryByParam = "fileName", Duration = ADay)]
        public ActionResult Image(string fileName)
        {
            fileName = Path.GetFileName(fileName); // تمیز سازی امنیتی است
            var rootPath = Server.MapPath("~/App_Data/Images");
            var path = Path.Combine(rootPath, fileName);
            if (!System.IO.File.Exists(path))
            {
                var notFoundImage = "notFound.png";
                path = Path.Combine(rootPath, notFoundImage);
                return File(path, MediaTypeNames.Image.Gif, notFoundImage);
            }
            return File(path, MediaTypeNames.Image.Gif, fileName);
        }
    }
}
در اینجا یک کنترلر را مشاهده می‌کنید که در اکشن متد Image آن، نام یک فایل دریافت شده و سپس این نام در پوشه App_Data/Images جستجو گردیده و نهایتا در مرورگر کاربر Flush می‌شود. از آنجائیکه الزامی ندارد fileName، واقعا یک fileName صحیح باشد، نیاز است توسط متد استاندارد Path.GetFileName این نام دریافتی اندکی تمیز شده و سپس مورد استفاده قرار گیرد. همچنین جهت کاهش بار سرور، از یک OutputCache به مدت یک روز نیز استفاده گردیده است.
نحوه استفاده از این اکشن متد نیز به نحو زیر است:
<img src="@Url.Action(actionName: "Image", controllerName: "Home", routeValues: new { fileName = "EF_Stra_08.gif" })" />


ب) آیا فراخوان تصویر ما را مستقیما در سایت خودش قرار داده است؟

        private bool isEmbeddedIntoAnotherDomain
        {
            get
            {
                return this.HttpContext.Request.UrlReferrer != null &&
                       !this.HttpContext.Request.Url.Host.Equals(this.HttpContext.Request.UrlReferrer.Host,
                                                                   StringComparison.InvariantCultureIgnoreCase);
            }
        }
در ادامه توسط خاصیت سفارشی isEmbeddedIntoAnotherDomain درخواهیم یافت که درخواست رسیده، از دومین جاری صادر شده است یا خیر. اینکار توسط بررسی UrlReferrer ارسال شده توسط مرورگر صورت می‌گیرد. اگر Host این UrlReferrer با Host درخواست جاری یکی بود، یعنی تصویر از سایت خودمان فراخوانی شده‌است.


ج) افزودن خودکار Watermark در صورت کپی شدن در سایتی دیگر

        private byte[] addWaterMark(string filePath, string text)
        {
            var image = new WebImage(filePath);
            image.AddTextWatermark(text);
            return image.GetBytes();
        }
کلاسی در فضای نام System.Web.Helpers وجود دارد به نام WebImage که کار افزودن Watermark را بسیار ساده کرده است. نمونه‌ای از نحوه استفاده از آن‌را در متد فوق ملاحظه می‌کنید.
اما ... پس از امتحان تصاویر مختلف ممکن است گاها با خطای زیر مواجه شویم:
 A Graphics object cannot be created from an image that has an indexed pixel format.
مشکل از اینجا است که تصاویر با فرمت ذیل برای انجام کار Watermark پشتیبانی نمی‌شوند:
PixelFormatUndefined
PixelFormatDontCare
PixelFormat1bppIndexed
PixelFormat4bppIndexed
PixelFormat8bppIndexed
PixelFormat16bppGrayScale
PixelFormat16bppARGB1555
اما می‌توان تصویر دریافتی را ابتدا تبدیل به BMP کرد و سپس Watermark دار نمود:
        private byte[] addWaterMark(string filePath, string text)
        {
            using (var img = System.Drawing.Image.FromFile(filePath))
            {
                using (var memStream = new MemoryStream())
                {
                    using (var bitmap = new Bitmap(img))//avoid gdi+ errors
                    {
                        bitmap.Save(memStream, ImageFormat.Png);                        
                        var webImage = new WebImage(memStream);
                        webImage.AddTextWatermark(text, verticalAlign: "Top", horizontalAlign: "Left", fontColor: "Brown");
                        return webImage.GetBytes();
                    }
                }
            }
        }
در اینجا نمونه اصلاح شده متد addWaterMark فوق را بر اساس کار با تصاویر bmp و سپس تبدیل آن‌ها به png، ملاحظه می‌کنید. به این ترتیب دیگر به خطای یاد شده بر نخواهیم خورد.
در ادامه، قسمت آخر کار، اعمال این مراحل به اکشن متد Image است:
            if (isEmbeddedIntoAnotherDomain)
            {
                var text = Url.Action(actionName: "Index", controllerName: "Home", routeValues: null, protocol: "http");
                var content = addWaterMark(path, text);
                return File(content, MediaTypeNames.Image.Gif, fileName);
            }
            return File(path, MediaTypeNames.Image.Gif, fileName);
در اینجا اگر تشخیص داده شود که تصویر، در دومین دیگری لینک شده است، آدرس سایت ما به صورت خودکار در بالای تصویر درج خواهد شد.

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


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

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


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


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

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


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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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


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

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

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

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

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

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

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

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

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-18.zip
مطالب
صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid
پس از آشنایی مقدماتی با Kendo UI DataSource، اکنون می‌خواهیم از آن جهت صفحه بندی، مرتب سازی و جستجوی پویای سمت سرور استفاده کنیم. در مثال قبلی، هر چند صفحه بندی فعال بود، اما پس از دریافت تمام اطلاعات، این اعمال در سمت کاربر انجام و مدیریت می‌شد.



مدل برنامه

در اینجا قصد داریم لیستی را با ساختار کلاس Product در اختیار Kendo UI گرید قرار دهیم:
namespace KendoUI03.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}


پیشنیاز تامین داده مخصوص Kendo UI Grid

برای ارائه اطلاعات مخصوص Kendo UI Grid، ابتدا باید درنظر داشت که این گرید، درخواست‌های صفحه بندی خود را با فرمت ذیل ارسال می‌کند. همانطور که مشاهده می‌کنید، صرفا یک کوئری استرینگ با فرمت JSON را دریافت خواهیم کرد:
 /api/products?{"take":10,"skip":0,"page":1,"pageSize":10,"sort":[{"field":"Id","dir":"desc"}]}
سپس این گرید نیاز به سه فیلد، در خروجی JSON نهایی خواهد داشت:
{
"Data":
[
{"Id":1500,"Name":"نام 1500","Price":2499.0,"IsAvailable":false},
{"Id":1499,"Name":"نام 1499","Price":2498.0,"IsAvailable":true}
],
"Total":1500,
"Aggregates":null
}
فیلد Data که رکوردهای گرید را تامین می‌کنند. فیلد Total که بیانگر تعداد کل رکوردها است و Aggregates که برای گروه بندی بکار می‌رود.

می‌توان برای تمام این‌ها، کلاس و Parser تهیه کرد و یا ... پروژه‌ی سورس بازی به نام  Kendo.DynamicLinq نیز چنین کاری را میسر می‌سازد که در ادامه از آن استفاده خواهیم کرد. برای نصب آن تنها کافی است دستور ذیل را صادر کنید:
 PM> Install-Package Kendo.DynamicLinq
Kendo.DynamicLinq به صورت خودکار System.Linq.Dynamic را نیز نصب می‌کند که از آن جهت صفحه بندی پویا استفاده خواهد شد.


تامین کننده‌ی داده سمت سرور

همانند مطلب کار با Kendo UI DataSource ، یک ASP.NET Web API Controller جدید را به پروژه اضافه کنید و همچنین مسیریابی‌های مخصوص آن‌را به فایل global.asax.cs نیز اضافه نمائید.
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using Kendo.DynamicLinq;
using KendoUI03.Models;
using Newtonsoft.Json;

namespace KendoUI03.Controllers
{
    public class ProductsController : ApiController
    {
        public DataSourceResult Get(HttpRequestMessage requestMessage)
        {
            var request = JsonConvert.DeserializeObject<DataSourceRequest>(
                requestMessage.RequestUri.ParseQueryString().GetKey(0)
            );

            var list = ProductDataSource.LatestProducts;
            return list.AsQueryable()
                       .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter);
        }
    }
}
تمام کدهای این کنترلر همین چند سطر فوق هستند. با توجه به ساختار کوئری استرینگی که در ابتدای بحث عنوان شد، نیاز است آن‌را توسط کتابخانه‌ی JSON.NET تبدیل به یک نمونه از DataSourceRequest نمائیم. این کلاس در Kendo.DynamicLinq تعریف شده‌است و حاوی اطلاعاتی مانند take و skip کوئری LINQ نهایی است.
ProductDataSource.LatestProducts صرفا یک لیست جنریک تهیه شده از کلاس Product است. در نهایت با استفاده از متد الحاقی جدید ToDataSourceResult، به صورت خودکار مباحث صفحه بندی سمت سرور به همراه مرتب سازی اطلاعات، صورت گرفته و اطلاعات نهایی با فرمت DataSourceResult بازگشت داده می‌شود. DataSourceResult نیز در Kendo.DynamicLinq تعریف شده و سه فیلد یاد شده‌ی Data، Total و Aggregates را تولید می‌کند.

تا اینجا کارهای سمت سرور این مثال به پایان می‌رسد.


تهیه View نمایش اطلاعات ارسالی از سمت سرور

اعمال مباحث بومی سازی
<head>
    <meta charset="utf-8" />
    <meta http-equiv="Content-Language" content="fa" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <title>Kendo UI: Implemeting the Grid</title>

    <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" />
    <!--شیوه نامه‌ی مخصوص راست به چپ سازی-->
    <link href="styles/kendo.rtl.min.css" rel="stylesheet" />
    <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" />
    <script src="js/jquery.min.js" type="text/javascript"></script>
    <script src="js/kendo.all.min.js" type="text/javascript"></script>

    <!--محل سفارشی سازی پیام‌ها و مسایل بومی-->
    <script src="js/cultures/kendo.culture.fa-IR.js" type="text/javascript"></script>
    <script src="js/cultures/kendo.culture.fa.js" type="text/javascript"></script>
    <script src="js/messages/kendo.messages.en-US.js" type="text/javascript"></script>

    <style type="text/css">
        body {
            font-family: tahoma;
            font-size: 9pt;
        }
    </style>

    <script type="text/javascript">
        // جهت استفاده از فایل: kendo.culture.fa-IR.js
        kendo.culture("fa-IR");
    </script>
</head>
- در اینجا چند فایل js و css جدید اضافه شده‌اند. فایل kendo.rtl.min.css جهت تامین مباحث RTL توکار Kendo UI کاربرد دارد.
- سپس سه فایل kendo.culture.fa-IR.js، kendo.culture.fa.js و kendo.messages.en-US.js نیز اضافه شده‌اند. فایل‌های fa و fa-Ir آن هر چند به ظاهر برای ایران طراحی شده‌اند، اما نام ماه‌های موجود در آن عربی است که نیاز به ویرایش دارد. به همین جهت به سورس این فایل‌ها، جهت ویرایش نهایی نیاز خواهد بود که در پوشه‌ی src\js\cultures مجموعه‌ی اصلی Kendo UI موجود هستند (ر.ک. فایل پیوست).
- فایل kendo.messages.en-US.js حاوی تمام پیام‌های مرتبط با Kendo UI است. برای مثال«رکوردهای 10 تا 15 از 1000 ردیف» را در اینجا می‌توانید به فارسی ترجمه کنید.
- متد kendo.culture کار مشخص سازی فرهنگ بومی برنامه را به عهده دارد. برای مثال در اینجا به fa-IR تنظیم شده‌است. این مورد سبب خواهد شد تا از فایل kendo.culture.fa-IR.js استفاده گردد. اگر مقدار آن‌را به fa تنظیم کنید، از فایل kendo.culture.fa.js کمک گرفته خواهد شد.

راست به چپ سازی گرید
تنها کاری که برای راست به چپ سازی Kendo UI Grid باید صورت گیرد، محصور سازی div آن در یک div با کلاس مساوی k-rtl است:
    <div class="k-rtl">
        <div id="report-grid"></div>
    </div>
k-rtl و تنظیمات آن در فایل kendo.rtl.min.css قرار دارند که در ابتدای head صفحه تعریف شده‌است.

تامین داده و نمایش گرید

در ادامه کدهای کامل DataSource و Kendo UI Grid را ملاحظه می‌کنید:
    <script type="text/javascript">
        $(function () {
            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    },
                    parameterMap: function (options) {
                        return kendo.stringify(options);
                    }
                },
                schema: {
                    data: "Data",
                    total: "Total",
                    model: {
                        fields: {
                            "Id": { type: "number" }, //تعیین نوع فیلد برای جستجوی پویا مهم است
                            "Name": { type: "string" },
                            "IsAvailable": { type: "boolean" },
                            "Price": { type: "number" }
                        }
                    }
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                pageSize: 10,
                sort: { field: "Id", dir: "desc" },
                serverPaging: true,
                serverFiltering: true,
                serverSorting: true
            });

            $("#report-grid").kendoGrid({
                dataSource: productsDataSource,
                autoBind: true,
                scrollable: false,
                pageable: true,
                sortable: true,
                filterable: true,
                reorderable: true,
                columnMenu: true,
                columns: [
                    { field: "Id", title: "شماره", width: "130px" },
                    { field: "Name", title: "نام محصول" },
                    {
                        field: "IsAvailable", title: "موجود است",
                        template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>'
                    },
                    { field: "Price", title: "قیمت", format: "{0:c}" }
                ]
            });
        });
    </script>
- با تعاریف مقدماتی Kendo UI DataSource پیشتر آشنا شده‌ایم و قسمت read آن جهت دریافت اطلاعات از سمت سرور کاربرد دارد.
- در اینجا ذکر contentType الزامی است. زیرا ASP.NET Web API بر این اساس است که تصمیم می‌گیرد، خروجی را به صورت JSON ارائه دهد یا XML.
- با استفاده از parameterMap، سبب خواهیم شد تا پارامترهای ارسالی به سرور، با فرمت صحیحی تبدیل به JSON شده و بدون مشکل به سرور ارسال گردند.
- در قسمت schema باید نام فیلدهای موجود در DataSourceResult دقیقا مشخص شوند تا گرید بداند که data را باید از چه فیلدی استخراج کند و تعداد کل ردیف‌ها در کدام فیلد قرار گرفته‌است.
- نحوه‌ی تعریف model را نیز در اینجا ملاحظه می‌کنید. ذکر نوع فیلدها در اینجا بسیار مهم است و اگر قید نشوند، در حین جستجوی پویا به مشکل برخواهیم خورد. زیرا پیش فرض نوع تمام فیلدها string است و در این حالت نمی‌توان عدد 1 رشته‌ای را با یک فیلد از نوع int در سمت سرور مقایسه کرد.
- در اینجا serverPaging، serverFiltering و serverSorting نیز به true تنظیم شده‌اند. اگر این مقدار دهی‌ها صورت نگیرد، این اعمال در سمت کلاینت انجام خواهند شد.

پس از تعریف DataSource، تنها کافی است آن‌را به خاصیت dataSource یک kendoGrid نسبت دهیم.
- autoBind: true سبب می‌شود تا اطلاعات DataSource بدون نیاز به فراخوانی متد read آن به صورت خودکار دریافت شوند.
- با تنظیم scrollable: false، اعلام می‌کنیم که قرار است تمام رکوردها در معرض دید قرارگیرند و اسکرول پیدا نکنند.
- pageable: true صفحه بندی را فعال می‌کند. این مورد نیاز به تنظیم pageSize: 10 در قسمت DataSource نیز دارد.
- با sortable: true مرتب سازی ستون‌ها با کلیک بر روی سرستون‌ها فعال می‌گردد.
- filterable: true به معنای فعال شدن جستجوی خودکار بر روی فیلدها است. کتابخانه‌ی Kendo.DynamicLinq حاصل آن‌را در سمت سرور مدیریت می‌کند.
- reorderable: true سبب می‌شود تا کاربر بتواند محل قرارگیری ستون‌ها را تغییر دهد.
- ذکر columnMenu: true اختیاری است. اگر ذکر شود، امکان مخفی سازی انتخابی ستون‌ها نیز مسیر خواهد شد.
- در آخر ستون‌های گرید مشخص شده‌اند. با تعیین "{format: "{0:c سبب نمایش فیلدهای قیمت با سه رقم جدا کننده خواهیم شد. مقدار ریال آن از فایل فرهنگ جاری تنظیم شده دریافت می‌گردد. با استفاده از template تعریف شده نیز سبب نمایش فیلد bool به صورت یک checkbox خواهیم شد.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
KendoUI03.zip
مطالب
لینک‌های هفته آخر آذر

وبلاگ‌ها و سایت‌های ایرانی


امنیت


Visual Studio


ASP. Net


طراحی وب


PHP

  • Aptana PHP 1.0 منتشر شد (اگر قبلا این IDE بسیار قابل توجه را دریافت کرده بودید فقط کافی است به منوی aptana و گزینه my aptana مراجعه کرده و از قسمت plugins ، این پلاگین 18 مگابایتی را دریافت کنید.)

اس‌کیوال سرور


سی شارپ


عمومی دات نت


ویندوز


متفرقه


نظرات مطالب
تهیه قالب برای ارسال ایمیل‌ها در ASP.NET Core توسط Razor Viewها
با تشکر از شما این مقاله را دیده بودم ولی مشکل من را حل نکرد . شاید بشه بهتر بگم که در :
var viewEngineResult = _viewEngine.FindView(actionContext, viewNameOrPath, isMainPage: false);
            if (!viewEngineResult.Success)
            {
                viewEngineResult = _viewEngine.GetView("~/", viewNameOrPath, isMainPage: false);
                if (!viewEngineResult.Success)
                {
                    throw new FileNotFoundException($"Couldn't find '{viewNameOrPath}'");
                }
            }

            var view = viewEngineResult.View;
بشه از :
var view = CreateViewFromString(@"@model int <div class="AAA">@Model</div>");
استفاده کرد.

نظرات مطالب
ASP.NET MVC #12
سلام آقای نصیری
در قسمتی که از radio استفاده کردید وقتی که model رو از نوع bool قرار میدم error مید
The model item passed into the dictionary is null
وقتی create  فراخوانی میشه و بعدش partial رو فراخوانی میکنه . مدل partial view نال هست
اما وقتی که partial view  رو بدون مدل set کردم مشکلی پیش نیومد و به فیلدم bind شد
میشه لطف کنید بگید اشکال کارم کجاست