نظرات مطالب
OpenCVSharp #6
- وجود Thread Sleep با مقداری که در مطلب فوق عنوان شده، ضروری هست. از این جهت که اساسا رابط کاربری معمولی ویندوز، قابلیت پردازش تعداد عظیمی از پیام‌های رسیده را ندارد و باید در این بین به آن فرصت داد. بحث DirectShow مجموعه‌ی Direct-X متفاوت است و طراحی اختصاصی آن برای یک چنین کارهایی است. اما در اینجا نمی‌توانید UI معمولی را با سیلی از داده‌ها و پیام‌های به روز رسانی، مدفون کنید.
- استفاده از متد capture.QueryFrame().ToBitmap اشتباه هست. از این جهت که خروجی capture.QueryFrame می‌تواند نال باشد. بنابراین این تبدیل را باید در داخل حلقه انجام دهید و نه در زمانیکه قصد دارید تصویری را دریافت کنید. شرط موجود در حلقه (مانند مثال اصلی مطلب)، بررسی نال نبودن این فریم دریافتی است. بنابراین اگر نال باشد، حلقه پایان خواهد یافت.
- همانطور که در متن عنوان شد، متد workerProgressChanged در ترد اصلی یا همان ترد UI اجرا می‌شود. بنابراین فراخوانی pictureBoxIpl1.Invoke غیر ضروری است و سربار بی‌جهتی را به سیستم تحمیل می‌کند.
به صورت خلاصه در حین استفاده‌ی از BackgroundWorker:
- متد رخداد گردان DoWork بر روی ThreadPool اجرا می‌شود (ترد آن با ترد UI یکی نیست)
- متد‌های رخدادگردان گزارش پیشرفت کار و اتمام کار، بر روی ترد UI اجرا می‌شوند. بنابراین امکان دسترسی به عناصر UI در این متدها، بدون مشکلی وجود دارد.
نظرات مطالب
نمایش تعداد کل صفحات در iTextSharp
به این صورت قابل انجام است:
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
    public class PdfWriterPageEvents : PdfPageEventHelper
    {
        PdfContentByte _pdfContentByte;
        // عدد نهایی تعداد کل صفحات را در این قالب قرار خواهیم داد
        PdfTemplate _template;
        Font _font;
        public override void OnOpenDocument(PdfWriter writer, Document document)
        {
            FontFactory.Register(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf");
            _font = FontFactory.GetFont("Tahoma", BaseFont.IDENTITY_H, embedded: true, size: 9);
            _pdfContentByte = writer.DirectContent;
            _template = _pdfContentByte.CreateTemplate(50, 50);
        }

        public override void OnEndPage(PdfWriter writer, Document document)
        {
            base.OnEndPage(writer, document);

            var pageSize = document.PageSize;
            var text = "صفحه " + writer.PageNumber + " از ";
            var textLen = _font.BaseFont.GetWidthPoint(text, _font.Size);
            var center = (pageSize.Left + pageSize.Right) / 2;

            ColumnText.ShowTextAligned(
                _pdfContentByte,
                Element.ALIGN_RIGHT,
                new Phrase(text, _font),
                center,
                pageSize.GetBottom(25),
                0,
                PdfWriter.RUN_DIRECTION_RTL,
                0);

            //در پایان هر صفحه یک جای خالی را مخصوص تعداد کل صفحات رزرو خواهیم کرد
            _pdfContentByte.AddTemplate(_template, center - textLen, pageSize.GetBottom(25));
        }
        public override void OnCloseDocument(PdfWriter writer, Document document)
        {
            base.OnCloseDocument(writer, document);
            _template.BeginText();
            _template.SetFontAndSize(_font.BaseFont, _font.Size);
            _template.SetTextMatrix(0, 0);
            //درج تعداد کل صفحات در تمام قالب‌های اضافه شده
            _template.ShowText((writer.PageNumber - 1).ToString());
            _template.EndText();
        }
    }

    public class AddTotalNoPages
    {
        public static void CreateTestPdf()
        {
            using (var pdfDoc = new Document(PageSize.A4))
            {
                var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("tpn.pdf", FileMode.Create));
                pdfWriter.PageEvent = new PdfWriterPageEvents();
                pdfDoc.Open();


                pdfDoc.Add(new Phrase("Page1"));
                pdfDoc.NewPage();
                pdfDoc.Add(new Phrase("Page2"));
                pdfDoc.NewPage();
                pdfDoc.Add(new Phrase("Page3"));
            }

            System.Diagnostics.Process.Start("tpn.pdf");
        }
    }
}

مطالب
سازگار کردن GridView با افزونه‌های jQuery


افزونه‌ها/پلاگین‌های زیادی جهت کار با table استاندارد HTML وجود دارند و خروجی رندر شده‌ی یک ASP.Net GridView هم در نهایت یک جدول است. فرض کنید قصد داریم افزونه زیر را به GridView استاندارد ASP.Net اعمال کنیم.
jQuery quickSearch plug-in

ظاهرا بدون مشکل خاصی اعمال می‌گردد. برای مثال در هدر صفحه داریم: (شبیه به مثال موجود در سایت اصلی آن، جهت اعمال به GridView1)

<script src="jquery.min.js" type="text/javascript"></script>
<script src="jquery.quicksearch.js" type="text/javascript"></script>

<script type="text/javascript">
$(document).ready(function() {
//جستجو در جدول
$('table#<%=GridView1.ClientID %> tbody tr').quicksearch({
position: 'before',
attached: 'span#attachSearch',
labelText: 'جستجو',
isFieldset: true,
loaderText: ' ... ',
fixWidths: true
});
});
</script>

برای اعمال بر:
(در اینجا محل قرارگیری تکست باکس مربوط به جستجو، در span ایی با id مساوی attachSearch تنظیم شده است، می‌توانید از ID خود GridView هم استفاده کنید.)

<span id="attachSearch"></span>
<br />
<asp:GridView ID="GridView1" runat="server"></asp:GridView>

نکته:
1- همانطور که در مقاله مربوط به ClientID ذکر شد، هیچ الزامی ندارد که ID‌ مربوط به GridView‌ شما برای مثال دقیقا همان GridView1 جهت استفاده در سمت کلاینت باشد و بسته به container آن، این نام ترکیبی از ID شیء(های) در بر گیرنده و شیء مورد نظر می‌باشد. به همین جهت از GridView1.ClientID استفاده گردید تا اسکریپت ما با آن مشکلی نداشته باشد.

2- خصوصیات ظاهری افزونه فوق از سلکتور quicksearch فایل css شما دریافت می‌شوند. برای مثال:
.quicksearch
{
width:190px;
}

مشکل!
پس از هر بار جستجو، header مربوط به GridView محو می‌شود، اما نمونه موجود در سایت اصلی افزونه، این مشکل را ندارد. چرا؟!
GridView‌ مربوط به ASP.Net پس از رندر شدن، جدولی است که تگ‌های thead را ندارد. اگر به سورس صفحه سایت افزونه دقت نمائید، هدر جدول با تگ‌های thead محصور شده است اما GridView استاندارد ASP.Net به صورت پیش فرض این‌کار را انجام نمی‌دهد و خروجی آن چیزی شبیه به جدول زیر است: (هدر با th مشخص می‌شود و از تگ thead خبری نیست)

<table >
<tr >
<th scope="col">col1</th>
<th scope="col">col2</th>
<th scope="col">col3</th>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
.
.
.

حداقل دو راه حل برای رفع این مشکل وجود دارد:
الف) راه حل سمت سرور
برای اضافه کردن thead باید کمی کد نویسی کرد. پس از اینکه گرید شما بایند شد، چند سطر زیر را اضافه کنید:

//سازگار با افزونه‌های جی کوئری
if (GridView1.Rows.Count > 0)
{
//This replaces <td> with <th> and adds the scope attribute
GridView1.UseAccessibleHeader = true;

//This will add the <thead> and <tbody> elements
GridView1.HeaderRow.TableSection = TableRowSection.TableHeader;

//This adds the <tfoot> element.
//Remove if you don't have a footer row
//GridView1.FooterRow.TableSection = TableRowSection.TableFooter;
}

ب)راه حل سمت کلاینت
سطر مربوط به جستجو را به صورت زیر هم می‌توان نوشت:

$('table#<%=GridView1.ClientID %> tr:has(td)').quicksearch({

در اینجا به دنبال هرچی tr که td دارد می‌گردیم. به صورت پیش فرض در tr مربوط به هدر گرید، ما th داریم و نه td ، بنابراین مشکل محو شدن هدر به این صورت حل خواهد شد.

نکته:
اگر می‌خواهید که ناحیه مربوط به جستجوی افزونه فوق در پرینت صفحه ظاهر نشود به css صفحه چند سطر زیر را اضافه کنید:

@media print
{
.quicksearch
{
display: none;
}
}


تمرین!
افزونه جی کوئری زیر را به یک ASP.Net GridView اعمال کنید:
table sorter

مطالب
آشنایی با WPF قسمت سوم: Layouts بخش دوم

  در مقاله قبلی در مورد تعدادی از Layout‌ها صحبت کردیم و در این بخش به ادامه‌ی آن پرداخته و دو مبحث GridPanel و Custom Layout را بررسی می‌کنیم.


GridPanel

پنل پیش فرضی است که موقع ایجاد یک پروژه‌ جدید WPF ایجاد می‌شود. چیدمان این نوع پنل به صورت سطر و ستون است و کارکرد آن بسیار مشابه جداول در HTML می‌باشد؛ با این تفاوت که در اینجا انعطاف پذیری بیشتری وجود دارد. هر سلول می‌تواند شامل چندین کنترل شود و یا هر کنترل می‌تواند چندین سلول را به خود احتصاص دهند و حتی می‌تواند روی کنترل‌های دیگر قرار بگیرند و همپوشانی کنترل‌ها را داشته باشیم.

تگ Grid Panel شامل دو تگ برای تعریف سطرها و ستون‌ها می‌باشد با استفاده از تگ Row Definition و Column Definition به تعیین تعداد سطر و ستون‌ها و اندازه آن‌ها می‌پردازیم:
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="28" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="200" />
    </Grid.ColumnDefinitions>
</Grid>
گرید پنل بالا شامل 4 سطر و دو ستون است و تعیین اندازه آن‌ها توسط دو خاصیت Width و  Height مشخص شده است که نحوه مقداردهی آن‌ها به صورت زیر است:
Fixed : یک مقدار ثابت، مثل سطر آخری که در کد بالا قرار می‌گیرد. این مقدار بر اساس یک واحد منطقی است و نه پیکسل که در این مقاله قبلا بررسی کرده‌ایم.
Auto : به مقداری که احتیاج دارد فضایی را بخود اختصاص می‌دهد.
* : هر آنچه از فضای موجود باقی مانده است را به خود اختصاص می‌دهد. علامت ستاره یک واحد نسبی است؛ به این صورت که می‌توانید مقدار فضا را به صورت زیر نیز بیان کنید.*3 و *2 به این معنی است که از پنج قسمت فضای باقیمانده سه قسمت و بعدی دو قسمت  را به خود اختصاص می‌دهد. عبارت * با *1 برابر است. عموما با این علامت فضا را به شکل درصد بیان می‌کنند:
 <ColumnDefinition Width="69*" />   <!-- Take 69% of remainder -->
    <ColumnDefinition Width="31*"/> <!-- Take 31% of remainder -->

نحوه‌ی اضافه کردنالمان‌ها به گرید به صورت زیر پس از تعیین تعداد سطرها و ستون‌ها انجام می‌گیرد و جایگاه هر المان در ستون یا سطر مربوطه توسط یک attached Dependency Property به نام‌های Grid.Column یا Grid.Row صورت می‌گیرد. خصوصیات Horizontal alignment و vertical Alignment هم برای تعیین موقعیت قرار گیری اشیاء در سلول به کار می‌روند و فاصله‌ی آن‌ها (کنترل ها) از لبه‌های گرید با margin محاسبه می‌شود.
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="28" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="200" />
    </Grid.ColumnDefinitions>
    <Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
    <Label Grid.Row="1" Grid.Column="0" Content="E-Mail:"/>
    <Label Grid.Row="2" Grid.Column="0" Content="Comment:"/>
    <TextBox Grid.Column="1" Grid.Row="0" Margin="3" />
    <TextBox Grid.Column="1" Grid.Row="1" Margin="3" />
    <TextBox Grid.Column="1" Grid.Row="2" Margin="3" />
    <Button Grid.Column="1" Grid.Row="3" HorizontalAlignment="Right" 
            MinWidth="80" Margin="3" Content="Send"  />
</Grid>

تغییر اندازه در سمت کد هم می‌تواند توسط کدهای صورت گیرد.
Auto sized GridLength.Auto
Star sized new GridLength(1,GridUnitType.Star)
Fixed size new GridLength(100,GridUnitType.Pixel)
مثال:
Grid grid = new Grid();
 
ColumnDefinition col1 = new ColumnDefinition();
col1.Width = GridLength.Auto;
ColumnDefinition col2 = new ColumnDefinition();
col2.Width = new GridLength(1,GridUnitType.Star);
 
grid.ColumnDefinitions.Add(col1);
grid.ColumnDefinitions.Add(col2);

قابلیت تغییر اندازه‌ی سطر و ستون توسط کاربر
یکی از تگ‌های ویژه داخل گری،د تگ Grid Splitter است. برای قرارگیری تگ splitter ابتدا باید یک سطر یا ستون بین سطر و ستون هایی که میخواهید از یکدیگر جدا شوند ایجاد کنید و اندازه‌ی آن را auto تعیین کنید و سپس مانند بقیه‌ی اشیا توسط Grid.Column یا Grid.Row مانند کد زیر تگ splitter را به آن اختصاص دهید.
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Label Content="Left" Grid.Column="0" />
    <GridSplitter HorizontalAlignment="Right" 
                  VerticalAlignment="Stretch" 
                  Grid.Column="1" ResizeBehavior="PreviousAndNext"
                  Width="5" Background="#FFBCBCBC"/>
    <Label Content="Right" Grid.Column="2" />
</Grid>
خاصیت ResizeBehavior مشخص می‌کند که ستون یا سطرهای کناری کدام باید تغییر اندازه داشته باشند.
 BasedOnAlignment   مقدار پیش فرض این گزینه است و مشخص می‌کند سطر یا ستونی طرفی باید تغییر اندازه دهد که در Alignment آن آمده است
 CurrentAndNext   ستون یا سطر جاری  به همراه ستون یا سطر بعدی
 PreviousAndCurrent   ستون یا سطر جاری  به همراه ستون یا سطر قبلی
 PreviousAndNext   سطر یا ستون قبلی و بعدی که بهترین گزینه برای انتخاب است.

خاصیت ResizeDirection جهت تغییر اندازه را مشخص می‌کند که شامل سه مقدار Row,Column و Auto است که مقدار پیش فرض آن auto است و نیازی به ذکر آن نیست و خود سیستم میداند که باید تغییر اندازه در چه جهتی صورت بگیرد.


ساخت Custom Layout یا یک پنل سفارشی (اختصاصی)
در این دو قسمت، شما با پنل‌های متفاوتی آشنا شدید که قابلیت‌های مفیدی داشتند؛ ولی گاهی اوقات هیچ کدام از این‌ها به کار شما نمی‌آیند و دوست دارید پنلی داشته باشید که مطابق میل شما عمل کند. برای ساخت یک پنل سفارشی یک کلاس می‌سازیم که از کلاس Panel ارث بری می‌کند. در اینجا دو متد برای Override کردن وجود دارند:
MeasureOverride : تعیین اندازه پنل بر اساس اندازه تعیین شده برای المان‌های فرزند و فضای موجود.
ArrangeOverride: مرتب سازی المان‌ها در فضای موجود نهایی.

کد نمونه:
public class MySimplePanel : Panel
{
    // Make the panel as big as the biggest element
    protected override Size MeasureOverride(Size availableSize)
    {
        Size maxSize = new Size();
 
        foreach( UIElement child in InternalChildern)
        {
            child.Measure( availableSize );
            maxSize.Height = Math.Max( child.DesiredSize.Height, maxSize.Height);
            maxSize.Width= Math.Max( child.DesiredSize.Width, maxSize.Width);
        }
    }
 
    // Arrange the child elements to their final position
    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach( UIElement child in InternalChildern)
        {
            child.Arrange( new Rect( finalSize ) );
        }
    }
}
لینک‌های زیر تعدادی از پنل‌های سفارشی پر طرفدار هستند که بر روی اینترنت به اشتراک گذاشته شده اند:
TreeMapPanel
Animating Tile Panel
Radial Panel
Element Flow Panel
Ribbon Panel

خواصی که باید در Layout‌ها با آنها بیشتر آشنا شویم:
Horizontal & Vertical Alignment
با دادن این خاصیت به کنترل‌های موجود، نحوه قرار گیری و موقعیت آن‌ها مشخص می‌گردد. جدول زیر بر ساس انواع موقعیت‌های مختلف تشکیل شده است:

Margin & Padding
این خاصیت‌ها حتما برای شما آشنا هستند. خاصیت margin فاصله کنترل از لبه‌های Layout است و خاصیت Padding فاصله محتویات کنترل از لبه‌های کنترل است.

Clipping
در صورتی که خاصیت ClipToBounds پنل برابر False باشند به این معناست که المان‌ها میتوانند از لبه‌های پنل خارج شوند، در صورتی که برابر True باشد مقدار خارج شده نمایش نمی‌یابد.

Scrolling
موقعیکه از پنلی استفاده می‌کنید که با تمام شدن ناحیه‌اش روبرو شده‌اید ولی کنترل‌های داخلش هنوز ادامه دارند، نیاز به یک اسکرول به شدت احساس می‌شود. در این حالت می‌توان از ScrollViewer استفاده کرد.
<ScrollViewer>
    <StackPanel>
        <Button Content="First Item" />
        <Button Content="Second Item" />
        <Button Content="Third Item" />
    </StackPanel>
</ScrollViewer>



نظرات مطالب
چگونگی گزارشگیری از Business Objects مانند List توسط StimulSoft
این روش بسیار زمانبر است؛ تصور کنید می‌خواهید از چندین جدول با تعداد فیلد‌های زیاد گزارش‌های مختلف تهیه کنید. اگر قرار به ایجاد دستی همه‌ی این فیلد‌ها در BO باشد که فاتحه وقت شما خوانده است و همچنین احتمال خطای شما در انتخاب نام و نوع فیلد بسیار خواهد بود.
برای انجام بهتر این کار در Stimul Report باید ابتدا لیست جنریک مورد نظر را به گزارش ارسال کنیم و سپس آن را در حالت Design نمایش داده و پس از ایجاد عناصر گزارش آن را در محل مورد نظر ذخیره نماییم. (لیست ارسالی به صورت خودکار در گزارش‌ساز نشان داده خواهد شد و تنها کاری که باید کنید گرفتن و کشیدن فیلدهای مورد نظر به داخل Data-Header-Footer ریپورت است).
برای مثال (با فرض استفاده از reportViewer):
stiReportMain.Dictionary.Report.BusinessObjectsStore.Clear();
reportViewer.Report.BusinessObjectsStore.Clear();
stiReportMain.RegBusinessObject("MyCategory", "documents", list);
stiReportMain.Design();
(کد فوق فقط برای طراحی گزارش است و باید یکبار اجرا شود).
پس از اجرای برنامه و طراحی گزارش مورد نظر آن را ذخیره می‌کنیم و برای استفاده کافی‌است از کد زیر استفاده کنیم:
stiReportMain.Dictionary.Report.BusinessObjectsStore.Clear();
reportViewer.Report.BusinessObjectsStore.Clear();
stiReportMain.RegBusinessObject("MyCategory", "documents", list);
stiReportMain.Dictionary.SynchronizeBusinessObjects();
stiReportMain.Compile();
stiReportMain.RenderWithWpf(true); //Or Render();
نکته ۱: documents لیست جنریک دریافتی از EF یا هر ORM دیگست.
نکته ۲:در صورتی که از شی StiReport استفاده کنید حتی نیاز به دادن آدرس فایل گزارش نخواهید داشت و فایل گزارش درون برنامه شما ذخیره خواهد شد. برای ویرایش آن نیز می‌توانید از فلش ظاهر شده کنار StiReport آن را ویرایش و مجددا ذخیره کنید.
مطالب
Angular Material 6x - قسمت چهارم - نمایش پویای اطلاعات تماس‌ها
در قسمت قبل، یک لیست ثابت item 1/item 2/… را در sidenav نمایش دادیم. در این قسمت می‌خواهیم این لیست را با اطلاعات دریافت شده‌ی از سرور، پویا کنیم و همچنین با کلیک بر روی هر کدام، جزئیات آن‌ها را نیز در قسمت main-content نمایش دهیم.



تهیه سرویس اطلاعات پویای برنامه

سرویس Web API ارائه شده‌ی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشت‌های آن‌ها به سمت کلاینت باز می‌گرداند و ساختار موجودیت‌های آن‌ها به صورت زیر است:

موجودیت کاربر که یک رابطه‌ی one-to-many را با UserNotes دارد:
using System;
using System.Collections.Generic;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class User
    {
        public User()
        {
            UserNotes = new HashSet<UserNote>();
        }

        public int Id { set; get; }
        public DateTimeOffset BirthDate { set; get; }
        public string Name { set; get; }
        public string Avatar { set; get; }
        public string Bio { set; get; }

        public ICollection<UserNote> UserNotes { set; get; }
    }
}
و موجودیت یادداشت‌های کاربر که سر دیگر رابطه را تشکیل می‌دهد:
using System;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class UserNote
    {
        public int Id { set; get; }
        public DateTimeOffset Date { set; get; }
        public string Title { set; get; }

        public User User { set; get; }
        public int UserId { set; get; }
    }
}
در نهایت اطلاعات ذخیره شده‌ی در بانک اطلاعاتی توسط سرویس کاربران:
    public interface IUsersService
    {
        Task<List<User>> GetAllUsersIncludeNotesAsync();
        Task<User> GetUserIncludeNotesAsync(int id);
    }
در اختیار کنترلر Web API برنامه، برای ارائه‌ی به سمت کلاینت، قرار می‌گیرد:
namespace MaterialAspNetCoreBackend.WebApp.Controllers
{
    [Route("api/[controller]")]
    public class UsersController : Controller
    {
        private readonly IUsersService _usersService;

        public UsersController(IUsersService usersService)
        {
            _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            return Ok(await _usersService.GetAllUsersIncludeNotesAsync());
        }

        [HttpGet("{id:int}")]
        public async Task<IActionResult> Get(int id)
        {
            return Ok(await _usersService.GetUserIncludeNotesAsync(id));
        }
    }
}
کدهای کامل لایه سرویس، تنظیمات EF Core و تنظیمات ASP.NET Core این قسمت را از پروژه‌ی پیوستی انتهای بحث می‌توانید دریافت کنید.
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
 https://localhost:5001/api/users
یک چنین خروجی قابل مشاهده خواهد بود:


و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت می‌دهد.


تنظیم محل تولید خروجی Angular CLI

ساختار پوشه بندی پروژه‌ی جاری به صورت زیر است:


همانطور که ملاحظه می‌کنید، کلاینت Angular در یک پوشه‌است و برنامه‌ی سمت سرور ASP.NET Core در پوشه‌ای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشه‌ی wwwroot پروژه‌ی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش می‌کنیم:
"build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
تنظیم این outputPath به wwwroot پروژه‌ی وب سبب خواهد شد تا با صدور فرمان زیر:
 ng build --no-delete-output-path --watch
برنامه‌ی Angular در حالت watch (گوش فرا دادان به تغییرات فایل‌ها) کامپایل شده و سپس به صورت خودکار در پوشه‌ی MaterialAspNetCoreBackend.WebApp/wwwroot کپی شود. به این ترتیب پس از اجرای برنامه‌ی ASP.NET Core توسط دستور زیر:
 dotnet watch run
 این برنامه‌ی سمت سرور، در همان لحظه هم API خود را ارائه خواهد داد و هم هاست برنامه‌ی Angular می‌شود.
بنابراین دو صفحه‌ی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشه‌ی MaterialAngularClient) و در دومی dotnet watch run را در پوشه‌ی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا می‌شوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژه‌ها ایجاد کردید، صرفا همان قسمت کامپایل می‌شود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.


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

در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادل‌های کلاس‌های موجودیت‌های سمت سرور خود را به صورت اینترفیس‌هایی تایپ‌اسکریپتی تعریف کنیم:
ng g i contact-manager/models/user
ng g i contact-manager/models/user-note
این دستورات دو اینترفیس خالی کاربر و یادداشت‌های او را در پوشه‌ی جدید models ایجاد می‌کنند. سپس آن‌ها را به صورت زیر و بر اساس تعاریف سمت سرور آن‌ها، تکمیل می‌کنیم:
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote {
  id: number;
  title: string;
  date: Date;
  userId: number;
}
محتویات فایل contact-manager\models\user.ts :
import { UserNote } from "./user-note";

export interface User {
  id: number;
  birthDate: Date;
  name: string;
  avatar: string;
  bio: string;

  userNotes: UserNote[];
}


ایجاد سرویس Angular دریافت اطلاعات از سرور

ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد می‌کنیم:
 ng g s contact-manager/services/user --no-spec
که سبب ایجاد فایل user.service.ts در پوشه‌ی جدید services خواهد شد:
import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor() { }
}
قسمت providedIn آن مخصوص Angular 6x است و هدف از آن کم حجم‌تر کردن خروجی نهایی برنامه‌است؛ اگر از سرویسی که تعریف شده، در برنامه جائی استفاده نشده‌است. به این ترتیب دیگر نیازی نیست تا آن‌را به صورت دستی در قسمت providers ماژول جاری ثبت و معرفی کرد.
کدهای تکمیل شده‌ی UserService را در ذیل مشاهده می‌کنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, throwError } from "rxjs";
import { catchError, map } from "rxjs/operators";

import { User } from "../models/user";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor(private http: HttpClient) { }

  getAllUsersIncludeNotes(): Observable<User[]> {
    return this.http
      .get<User[]>("/api/users").pipe(
        map(response => response || []),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }

  getUserIncludeNotes(id: number): Observable<User> {
    return this.http
      .get<User>(`/api/users/${id}`).pipe(
        map(response => response || {} as User),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }
}
در اینجا از pipe-able operators مخصوص RxJS 6x استفاده شده که در مطلب «ارتقاء به Angular 6: بررسی تغییرات RxJS» بیشتر در مورد آن‌ها بحث شده‌است.
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشت‌های آن‌ها از سرور واکشی می‌کند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشت‌های او از سرور دریافت می‌کند.


بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material

بسته‌ی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوه‌ی «افزودن آیکن‌های متریال به برنامه» را بررسی کردیم که در آنجا آیکن‌های مرتبط، از فایل‌های قلم، دریافت و نمایش داده می‌شوند. این کامپوننت، علاوه بر قلم آیکن‌ها، از فایل‌های svg حاوی آیکن‌ها نیز پشتیبانی می‌کند که یک نمونه از این فایل‌ها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شده‌است (چون برنامه‌ی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
    <svg viewBox="0 0 128 128" height="100%" width="100%" 
             pointer-events="none" display="block" id="user1" >
هر svg تعریف شده‌ی در آن دارای یک id است. از این id به عنوان نام avatar کاربرها استفاده خواهیم کرد. نحوه‌ی فعالسازی آن نیز به صورت زیر است:
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماس‌ها را با صورت زیر تکمیل می‌کنیم:
import { Component } from "@angular/core";
import { MatIconRegistry } from "@angular/material";
import { DomSanitizer } from "@angular/platform-browser";

@Component()
export class ContactManagerAppComponent {

  constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
    iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg"));
  }
  
}
MatIconRegistry جزئی از بسته‌ی angular/material است که در ابتدای کار import شده‌است. متد addSvgIconSet آن، مسیر یک فایل svg حاوی آیکن‌های مختلف را دریافت می‌کند. این مسیر نیز باید توسط سرویس DomSanitizer در اختیار آن قرار گیرد که در کدهای فوق روش انجام آن‌را ملاحظه می‌کنید. در مورد سرویس DomSanitizer در مطلب «نمایش HTML در برنامه‌های Angular» بیشتر بحث شده‌است.
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon(
      "unicorn",
      this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg")
    );
که در نهایت کامپوننت mat-icon، این آیکن را به صورت زیر می‌تواند نمایش دهد:
 <mat-icon svgIcon="unicorn"></mat-icon>

یک نکته: پوشه‌ی node_modules\material-design-icons به همراه تعداد قابل ملاحظه‌ای فایل svg نیز هست.


نمایش لیست کاربران در sidenav

در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق می‌کنیم:
import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component()
export class SidenavComponent implements OnInit {

  users: User[] = [];

  constructor(private userService: UserService) {  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => this.users = data);
  }
}
به این ترتیب با اجرای برنامه و بارگذاری sidenav، در رخ‌داد OnInit آن، کار دریافت اطلاعات کاربران و انتساب آن به خاصیت عمومی users صورت می‌گیرد.

اکنون می‌خواهیم از این اطلاعات جهت نمایش پویای آن‌ها در sidenav استفاده کنیم. در قسمت قبل، جای آن‌ها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
    <mat-list>
      <mat-list-item>Item 1</mat-list-item>
      <mat-list-item>Item 2</mat-list-item>
      <mat-list-item>Item 3</mat-list-item>
    </mat-list>
اگر به مستندات mat-list مراجعه کنیم، در میانه‌ی صفحه، navigation lists نیز ذکر شده‌است که می‌تواند لیستی پویا را به همراه لینک به آیتم‌های آن نمایش دهد و این مورد دقیقا کامپوننتی است که در اینجا به آن نیاز داریم. بنابراین فایل sidenav\sidenav.component.html را گشوده و mat-list فوق را با mat-nav-list تعویض می‌کنیم:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine href="#">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
اکنون اگر برنامه را اجرا کنیم، یک چنین شکلی قابل مشاهده است:


که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آن‌ها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.


اتصال کاربران به صفحه‌ی نمایش جزئیات آن‌ها

در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شده‌است. در ادامه می‌خواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine [routerLink]="['/contactmanager', user.id]">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
در اینجا با استفاده از routerLink، هر کاربر را بر اساس Id او، به صفحه‌ی جزئیات آن شخص، متصل کرده‌ایم. البته این مسیریابی برای اینکه کار کند باید به صورت زیر به فایل contact-manager-routing.module.ts اضافه شود:
const routes: Routes = [
  {
    path: "", component: ContactManagerAppComponent,
    children: [
      { path: ":id", component: MainContentComponent },
      { path: "", component: MainContentComponent }
    ]
  },
  { path: "**", redirectTo: "" }
];
البته اگر تا اینجا برنامه را اجرا کنید، با نزدیک کردن اشاره‌گر ماوس به نام هر کاربر، آدرسی مانند https://localhost:5001/contactmanager/1 در status bar مرورگر ظاهر خواهد شد، اما با کلیک بر روی آن، اتفاقی رخ نمی‌دهد.
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کرده‌ایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل می‌کنیم:
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component({
  selector: "app-main-content",
  templateUrl: "./main-content.component.html",
  styleUrls: ["./main-content.component.css"]
})
export class MainContentComponent implements OnInit {

  user: User;
  constructor(private route: ActivatedRoute, private userService: UserService) { }

  ngOnInit() {
    this.route.params.subscribe(params => {
      this.user = null;

      const id = params["id"];
      if (!id) {
        return;
      }

      this.userService.getUserIncludeNotes(id)
        .subscribe(data => this.user = data);
    });
  }
}
در اینجا به کمک سرویس ActivatedRoute و گوش فرادادن به تغییرات params آن در ngOnInit، مقدار id مسیر دریافت می‌شود. سپس بر اساس این id، با کمک سرویس کاربران، اطلاعات این تک کاربر از سرور دریافت و به خاصیت عمومی user نسبت داده خواهد شد.
اکنون می‌توان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user">
  <mat-spinner></mat-spinner>
</div>
<div *ngIf="user">
  <mat-card>
    <mat-card-header>
      <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon>
      <mat-card-title>
        <h2>{{ user.name }}</h2>
      </mat-card-title>
      <mat-card-subtitle>
        Birthday {{ user.birthDate | date:'d LLLL' }}
      </mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <mat-tab-group>
        <mat-tab label="Bio">
          <p>
            {{user.bio}}
          </p>
        </mat-tab>
        <!-- <mat-tab label="Notes"></mat-tab> -->
      </mat-tab-group>
    </mat-card-content>
  </mat-card>
</div>
در اینجا از کامپوننت mat-spinner برای نمایش حالت منتظر بمانید استفاده کرده‌ایم. اگر user نال باشد، این spinner نمایش داده می‌شود و برعکس.


همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کرده‌ایم (اگر دقت کنید، هر کامپوننت آن سه برگه‌ی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره می‌توان با کامپوننت‌های این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا می‌کنیم که مناسب کار ما باشد. سپس سورس آن‌را از همانجا کپی و در برنامه قرار می‌دهیم و در آخر آن‌را بر اساس اطلاعات خود سفارشی سازی می‌کنیم.



نمایش جزئیات اولین کاربر در حین بارگذاری اولیه‌ی برنامه

تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماس‌ها ظاهر می‌شود و همانطور باقی می‌ماند، با اینکه هنوز موردی انتخاب نشده‌است. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظه‌ی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش می‌دهیم:
import { Router } from "@angular/router";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  users: User[] = [];
  
  constructor(private userService: UserService, private router: Router) {
  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => {
        this.users = data;
        if (data && data.length > 0 && !this.router.navigated) {
          this.router.navigate(["/contactmanager", data[0].id]);
        }
      });
  }
}
در اینجا ابتدا سرویس Router به سازنده‌ی کلاس تزریق شده‌است و سپس زمانیکه کار دریافت اطلاعات تماس‌ها پایان یافت و this.router.navigated نبود (یعنی پیشتر هدایت به آدرسی صورت نگرفته بود؛ برای مثال کاربر آدرس id داری را ریفرش نکرده بود)، اولین مورد را توسط متد this.router.navigate فعال می‌کنیم که سبب تغییر آدرس صفحه از https://localhost:5001/contactmanager به https://localhost:5001/contactmanager/1 و باعث نمایش جزئیات آن می‌شود.

البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id'];
if (!id) id = 1;
در اینجا اگر id انتخاب نشده باشد، یعنی اولین بار نمایش برنامه است و خودمان id مساوی 1 را برای آن در نظر می‌گیریم.


بستن خودکار sidenav در حالت نمایش موبایل

اگر اندازه‌ی صفحه‌ی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتم‌های آن‌را انتخاب کنیم، هرچند آن‌ها نمایش داده می‌شوند، اما زیر این sidenav مخفی باقی خواهند ماند:


بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینه‌های آن، این قسمت بسته شده و ناحیه‌ی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شده‌است:
<mat-sidenav #sidenav
برای دسترسی به آن در کدهای کامپوننت خواهیم داشت:
import { MatSidenav } from "@angular/material";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  @ViewChild(MatSidenav) sidenav: MatSidenav;
اکنون که به این ViewChild دسترسی داریم، می‌توانیم در حالت نمایشی موبایل، متد close آن‌را فراخوانی کنیم:
  ngOnInit() {
    this.router.events.subscribe(() => {
      if (this.isScreenSmall) {
        this.sidenav.close();
      }
    });
  }
در اینجا با مشترک this.router.events شدن، متوجه‌ی کلیک کاربر و نمایش صفحه‌ی جزئیات آن می‌شویم. در قسمت سوم این مجموعه نیز خاصیت isScreenSmall را بر اساس ObservableMedia مقدار دهی کردیم. بنابراین اگر کاربر بر روی گزینه‌ای کلیک کرده بود و همچنین اندازه‌ی صفحه در حالت موبایل قرار داشت، sidenav را خواهیم بست تا بتوان محتوای زیر آن‌را مشاهده کرد:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
مطالب
شروع به کار با EF Core 1.0 - قسمت 6 - تعیین نوع‌های داده و ویژگی‌های آن‌ها
یکی از مهم‌ترین قسمت‌های مدل سازی موجودیت‌ها، تعیین نوع‌های صحیح ستون‌ها و همچنین تعیین اندازه‌ی مناسبی برای آن‌ها است؛ به همراه تعیین اجباری بودن یا نبودن مقدار دهی آن‌ها.

تعیین اجباری بودن یا نبودن ستون‌ها در EF Core

به صورت پیش فرض در EF Core، هر نوع CLR ایی که نال پذیر باشد، به صورت یک ستون اختیاری در نظر گرفته می‌شود؛ مانند:
 string, int?, byte[]
و هر ستونی که نوع CLR آن نال پذیر نباشد، مقدار دهی آن در EF Core اجباری است؛ مانند:
 int, decimal, bool, DateTime
همچنین باید دقت داشت که حتی اگر در تنظیمات نگاشت‌های برنامه به صورت اختیاری تعریف شوند، باز هم EF Core آن‌ها را اجباری درنظر می‌گیرد.

برای لغو اختیاری بودن یک خاصیت نال پذیر می‌توان از ویژگی Required استفاده کرد:
 [Required]
public string Url { get; set; }
نوع string نال پذیر است. برای لغو این وضعیت می‌توان از ویژگی Required استفاده کرد که در سمت بانک اطلاعاتی نیز به not null ترجمه می‌شود.
و یا معادل Fluent API آن با استفاده از ذکر متد IsRequired است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Blog>()
              .Property(b => b.Url)
              .IsRequired();
}
با توجه به این توضیحات، نیازی نیست در بالای یک خاصیت از نوع int، ویژگی Required را ذکر کرد. چون int نال پذیر نیست، مقدار دهی آن اجباری است.


کار با رشته‌ها در EF Core

ذکر یک خاصیت رشته‌ای به این صورت:
public string FirstName { get; set; }
به معنای نال پذیر بودن این ستون است (چون Required تعریف نشده‌است) و همچنین نوع و طول آن در SQL Server به nvarchar max تنظیم می‌شود. این تنظیم طول هرچند در مورد SQL Server صادق است، اما ممکن است در SQL Server CE به nvarchar 4000 تفسیر شود (و این مشکل را به همراه داشته باشد که چرا نمی‌توان متون طولانی را در آن ثبت کرد). به عبارتی عدم ذکر دقیق طول یک خاصیت رشته‌ای، در پروایدرهای مختلف، ممکن است معانی مختلفی را به همراه داشته باشد. بنابراین نیاز است طول خواص رشته‌ای حتما ذکر شوند تا در تمام بانک‌های اطلاعاتی با دقت کامل و بدون حدس و گمان تنظیم گردند.
  [StringLength(450)]
  public string FirstName { get; set; }

  [MaxLength(450)]
  public string LastName { get; set; }

  [MaxLength]
  public string Address { get; set; }
برای تعیین طول دقیق یک فیلد رشته‌ای، می‌توان از ویژگی‌های StringLength و یا MaxLength با ذکر اندازه‌ای استفاده کرد.
برای تعیین صریح یک فیلد رشته‌ای به حداکثر مقدار آن بهتر است ویژگی MaxLength را بدون ذکر پارامتری قید کرد. این مورد جهت سازگاری با بانک‌های اطلاعاتی مختلف ضروری است.
معادل این تنظیمات با روش Fluent API به صورت زیر است:
برای تعیین صریح طول یک فیلد رشته‌ای:
modelBuilder.Entity<Person>()
   .Property(x => x.Address)
   .HasMaxLength(450);
و برای تعیین صریح nvarchar max بودن آن فیلد:
modelBuilder.Entity<Person>()
   .Property(x => x.Address)
   .HasColumnType("nvarchar(max)");
حالت پیش فرض EF Core، کار با رشته‌های یونیکد است. یعنی تمام فیلدهای فوق به nvarchar تفسیر می‌شوند و این n ایی که در ابتدا ذکر شده‌است به معنای یونیکد بودن آن است. اگر می‌خواهید این پیش‌فرض تعیین نوع یونیکد را تغییر دهید، می‌توان از ویژگی Column استفاده کرد:
   [Column(TypeName = "varchar")]
  [MaxLength]
  public string Address { get; set; }
البته اگر اطلاعاتی را که با آن کار می‌کنید چندزبانی و یونیکد هستند، بهتر است این مورد را تغییر ندهید.

نکته‌ای در مورد تغییر نوع خواص: اگر از متد HasColumnType و یا ویژگی Column به نحو فوق استفاده کردید، نیاز است طول رشته را صریحا مشخص کنید. در غیر اینصورت در حین migration خطای ذیل را دریافت خواهید کرد:
 Data type 'varchar' is not supported in this form. Either specify the length explicitly in the type name, for example as 'varchar(16)',
or remove the data type and use APIs such as HasMaxLength to allow EF choose the data type.
در اینجا عنوان می‌کند که اگر مقصود شما varchar max است، ویژگی MaxLength را حذف کرده و تنها بنویسید:
   [Column(TypeName = "varchar(max)")]

نکته‌ای در مورد ایندکس‌ها: در قسمت قبل عنوان شد که می‌توان بر روی خواص، ایندکس منحصربفرد اعمال کرد. در مورد رشته‌ها در SQL Server، اگر طول فیلد مدنظر حداکثر تا 900 بایت باشد، یک چنین کاری را می‌توان انجام داد. البته این محدودیت 900 بایتی تا SQL Server 2014 وجود دارد. این سقف در SQL Server 2016 به 1700 بایت افزایش یافته‌است (900bytes for a clustered index. 1,700 for a nonclustered index). بنابراین چون نوع پیش فرض ستون‌های رشته‌ای، یونیکد و nvarchar درنظر گرفته می‌شود، حداکثر طول امنی را که می‌توان برای آن تعریف کرد، مساوی 450 است (نصف 900 بایت). به همین جهت ذکر ایندکس منحصربفرد بر روی یک ستون رشته‌ای، باید به همراه ذکر اجباری حداکثر طول مساوی 450 آن باشد.


کار با اعداد در EF Core

کلاس نمونه‌ای را با ساختار ذیل درنظر بگیرید:
    public class Person 
    {
        public int Id { set; get; }

        public DateTime? DateAdded { set; get; }

        public DateTime? DateUpdated { set; get; }

        [StringLength(450)]
        public string FirstName { get; set; }

        [MaxLength(450)]
        public string LastName { get; set; }

        //[Column(TypeName = "varchar")]
        [MaxLength]
        public string Address { get; set; }


        //bit
        public bool IsActive { get; set; }

        //tiny Int
        public byte Age { get; set; }

        //small Int
        public short Short { get; set; }

        //int
        public int Int32 { get; set; }

        //Big int
        public long Long { get; set; }
    }
پس از اعمال مهاجرت‌ها و به روز رسانی ساختار بانک اطلاعاتی، به ساختار ذیل خواهیم رسید:


همانطور که ملاحظه می‌کنید، نوع bool دات نت به نوع bit در SQL Server، نوع long به bigint، نوع short به smallint، نوع int به int و نوع byte به tinyint نگاشت شده‌اند.


نکته‌ای در مورد اعداد اعشاری: توصیه شده‌است در تعاریف موجودیت‌های خود بهتر است از نوع‌های float و یا double استفاده نکنید. برای کار با اعداد اعشاری از نوع decimal استفاده نمائید تا بتوانید از قابلیت مقایسه‌ی دقیق آن‌ها استفاده کنید. اطلاعات بیشتر: «روش صحیح مقایسه دو عدد اعشاری با هم»


کار با تاریخ در EF Core

اگر به تصویر فوق دقت کنید، نوع DateTime دات نت به datetime2 در سمت SQL Server ترجمه شده‌است:
 CREATE TABLE [dbo].[Persons](
 [DateAdded] [datetime2](7) NULL,
 [DateUpdated] [datetime2](7) NULL,
اگر در داده‌های خود نیازی به زمان ندارید، می‌توان این نوع پیش فرض را با ویژگی Column که پیشتر بحث شد، به date تغییر داد.
اطلاعات بیشتر: «کنترل نوع‌های داده با استفاده از EF در SQL Server»

به علاوه در دات نت نوع DateTime از نوع value type است. بنابراین همانطور که در ابتدای بحث نیز عنوان شد، مقدار دهی آن اجباری است؛ مگر آنکه آن‌را نال پذیر تعریف کنید.


کار با مباحث همزمانی در EF Core

EF Core به صورت پیش فرض، فرض می‌کند رکوردی را که با آن در حال کار هستید، توسط هیچ کاربر دیگری در شبکه تغییر نیافته‌است و تغییرات شما را در حین فراخوانی متد SaveChanges ذخیره می‌کند. اگر علاقمند هستید که EF Core در صورت تغییر مقدار خاصیت خاصی توسط سایر کاربران، این مساله را با صدور استثنایی به شما اطلاع رسانی کند، از ویژگی ConcurrencyCheck
 [ConcurrencyCheck]
public string Name { set; get; }
و یا متد IsConcurrencyToken حالت Fluent API استفاده نمائید:
modelBuilder.Entity<Person>()
    .Property(p => p.Name)
    .IsConcurrencyToken();
در این حالت کوئری به روز رسانی، علاوه بر فیلد Id رکورد، حاوی فیلد Name نیز خواهد بود (در حین تشکیل شرط یافتن رکورد) و اگر در بین فاصله‌ی یافتن شخص و به روز رسانی نام او، شخص دیگری این‌کار را انجام داده باشد، این به روز رسانی موفقیت آمیز نبوده و استثنایی صادر می‌شود.

اگر علاقمند هستید که تمام فیلدهای جدول تحت نظر قرارگیرند، می‌توان از روش ویژه‌ای به نام Timestamp/row version استفاده کرد:
 [Timestamp]
 public byte[] Timestamp { get; set; }
با معادل Fluent API ذیل:
modelBuilder.Entity<Blog>()
   .Property(p => p.Timestamp)
   .ValueGeneratedOnAddOrUpdate()
   .IsConcurrencyToken();
در مورد ValueGeneratedOnAddOrUpdate در قسمت قبل بحث کردیم. فیلد TimeStamp نیز جزو فیلدهای ویژه‌ای است که SQL Server به صورت خودکار قادر است آن‌را مقدار دهی کند و زمانیکه ValueGeneratedOnAddOrUpdate قید می‌شود، یعنی این فیلد همواره با فراخوانی متد SaveChanges، به صورت خودکار مقدار دهی خواهد شد (و نیازی نیست تا توسط برنامه مقدار دهی شود).
در این حالت در حین به روز رسانی یک چنین رکوردی، اگر از زمان کوئری آن (یافتن رکورد) و ذخیره سازی آن، شخص دیگری آن‌را تغییر داده باشد، به علت عدم تطابق Timestamp ها، عملیات به روز رسانی باشکست روبرو شده و یک استثناء صادر می‌شود.
مطالب
BloggerToCHM 1.3

نگارش جدید برنامه BloggerToCHM را از اینجا می‌توانید دریافت کنید.
تغییرات حاصل شده:
  • پشتیبانی بهتر از تغییرات open search API که هر از چندگاهی توسط گوگل اعمال می‌شود.
  • رفع مشکل تخریب اعداد فارسی در فایل تولیدی نهایی
  • اضافه شدن چند گزینه جهت کنترل بر روی نمایش قسمت about در فایل نهایی حاصل و همچنین درج نظرات
  • و ...

پروژه‌ها
PersianCaptchaHandler
تولید تصویر امنیتی اعداد فارسی بحروف
تصویر امنیتی و یا کپچا برای تشخیص و احراز انسان بودن استفاده کننده استفاده میشود و بصورت تصویری که استخراج نوشته‌های درون آن برای  روبوت‌ها بسیار سخت و یا نشدنی است ایجاد میشود و دارای انواع و اقسام متفاوتی است. در این میان برای استفاده از این امکان نمونه هایی در زبانهای مختلف تهیه شده که بسته به سلیقه و نیاز مورد استفاده قرار گرفته شده است. 
نظرات مطالب
EF Code First #14
جناب نصیری ممنون از جوابتون، با روشی که میفرمایید(استفاده از DbContext در سطح فرم)چنانچه مدل من به شکل زیر باشه :

و صفحه ویرایش سفارش من به شکل زیر:

و کاربر پس از زدن دکمه ویرایش قادر به انجام کارهای زیر باشه:
  • ویرایش شماره سفارش
  • ویرایش یک OrderLine
  • حذف یک OrderLine
  • ویرایش یک OrderLine
  • اضافه کردن یک OrderLine 

و سپس بخواد دکمه ذخیره رو بزنه؛ برای اینکه کل تغییرات کاربر رو ذخیره کنم کدوم یک از روشهای زیر رو بایستی استفاده کنم؟  

  1. event‌های مناسبی رو پیدا کنم و به محض رخ دادن اونها بر اساس اونها تصمیم بگیرم که entity  مورد نظر بایستی در DbContext(اضافه/حذف و یا ویرایش) بشه?
  2. تا زمانی که کاربر دکمه ذخیره رو نزده، کاری با DbContext نداشته باشم و وقتی کاربر دکمه ذخیره رو زد، گراف(سفارش و آیتمهای سفارش) رو به DbContext بدم؟