نظرات مطالب
TwitterBootstrapMVC
- داره (مجانی نیست). البته فایلهایی که ایشون پیوست کردند به نظر محدودیت ندارند.
- ضمنا باز هم هستند یک سری Wrapper برای بوت استرپ که میتوانند مورد استفاده قرار گیرند:
TwitterBootstrapMvc (نسخه سورس باز مطلب جاری است)
Mvc Bootstrap Html Helper Extensions
Bootstrap Helpers (معرفی در اینجا)
Twitter Bootstrap Controls for ASP.NET
- ضمنا باز هم هستند یک سری Wrapper برای بوت استرپ که میتوانند مورد استفاده قرار گیرند:
TwitterBootstrapMvc (نسخه سورس باز مطلب جاری است)
Mvc Bootstrap Html Helper Extensions
Bootstrap Helpers (معرفی در اینجا)
Twitter Bootstrap Controls for ASP.NET
تا صحبت از گزارشگیری به میان بیاید احتمالا معرفی ابزارهای تجاری مانند Reporting services ، کریستال ریپورت، stimulsoft.com ، fast-report.com و امثال آن درصدر لیست توصیه کنندگان و مشاوران قرار خواهند داشت. اما خوب برای ایجاد یک گزارشگیری ساده حتما نیازی نیست تا به این نوع ابزارهای تجاری مراجعه کرد. ابزار رایگان و سورس باز جالبی هم در این باره جهت پروژههای WPF در دسترس است:
در ادامه در طی یک مثال قصد داریم از این کتابخانه استفاده کنیم:
1) تنظیم وابستگیها
پس از دریافت کتابخانه فوق، ارجاعات زیر باید به پروژه شما اضافه شوند:
CodeReason.Reports.dll (از پروژه فوق) و ReachFramework.dll (جزو اسمبلیهای استاندارد دات نت است)
2) تهیه منبع داده گزارش
کتابخانهی فوق به صورت پیش فرض با DataTable کار میکند. بنابراین کوئریهای شما یا باید خروجی DataTable داشته باشد یا باید از یک سری extension methods برای تبدیل IEnumerable به DataTable استفاده کرد (در پروژه پیوست شده در پایان مطلب، این موارد موجود است).
برای مثال فرض کنید میخواهیم رکوردهایی را از نوع کلاس Product زیر در گزارش نمایش دهیم:
namespace WpfRptTests.Model
{
public class Product
{
public string Name { set; get; }
public int Price { set; get; }
}
}
الف) اضافه کردن فایل تشکیل دهنده ساختار و ظاهر گزارش
گزارشهای این کتابخانه مبتنی است بر اشیاء FlowDocument استاندارد WPF . بنابراین از منوی پروژه گزینهی Add new item در قسمت WPF آن یک FlowDocument جدید را به پروژه اضافه کنید ( باید دقت داشت که Build action این فایل باید به Content تنظیم گردد). ساختار ابتدایی این FlowDocument به صورت زیر خواهد بود که به آن FlowDirection و FontFamily مناسب جهت گزارشات فارسی اضافه شده است. همچنین فضای نام مربوط به کتابخانهی گزارشگیری CodeReason.Reports نیز باید اضافه گردد.
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FlowDirection="RightToLeft" FontFamily="Tahoma"
xmlns:xrd="clr-namespace:CodeReason.Reports.Document;assembly=CodeReason.Reports"
PageHeight="29.7cm" PageWidth="21cm" ColumnWidth="21cm">
</FlowDocument>
مواردی که در ادامه ذکر خواهند شد محتوای این گزارش را تشکیل میدهند:
ب) مشخص سازی خواص گزارش
<xrd:ReportProperties>
<xrd:ReportProperties.ReportName>SimpleReport</xrd:ReportProperties.ReportName>
<xrd:ReportProperties.ReportTitle>گزارش از محصولات</xrd:ReportProperties.ReportTitle>
</xrd:ReportProperties>
ج) مشخص سازی Page Header و Page Footer
اگر میخواهید عباراتی در بالا و پایین تمام صفحات گزارش تکرار شوند میتوان از SectionReportHeader و SectionReportFooter این کتابخانه به صورت زیر استفاده کرد:
<xrd:SectionReportHeader PageHeaderHeight="2" Padding="10,10,10,0" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportHeader>
<xrd:SectionReportFooter PageFooterHeight="2" Padding="10,0,10,10" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
نام کاربر:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
صفحه
<xrd:InlineContextValue PropertyName="PageNumber" FontWeight="Bold" /> از
<xrd:InlineContextValue PropertyName="PageCount" FontWeight="Bold" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportFooter>
دو نکته در اینجا حائز اهمیت هستند: xrd:InlineDocumentValue و xrd:InlineContextValue
InlineDocumentValue را میتوان در کدهای برنامه به صورت سفارشی اضافه کرد. بنابراین هر جایی که نیاز بود مقدار ثابتی از طریق کد نویسی به گزارش تزریق و اضافه شود میتوان از InlineDocumentValue استفاده کرد. برای مثال در کدهای ViewModel برنامه که در ادامه ذکر خواهد شد دو مقدار PrintDate و RptBy به صورت زیر تعریف و مقدار دهی شدهاند:
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحید");
د) مشخص سازی ساختار تولیدی گزارش
<Section Padding="80,10,40,10" FontSize="12">
<Paragraph FontSize="24" TextAlignment="Center" FontWeight="Bold">
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
<Paragraph TextAlignment="Center">
گزارش از لیست محصولات در تاریخ:
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
توسط:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
<xrd:SectionDataGroup DataGroupName="ItemList">
<Table CellSpacing="0" BorderBrush="Black" BorderThickness="0.02cm">
<Table.Resources>
<!-- Style for header/footer rows. -->
<Style x:Key="headerFooterRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Background" Value="LightGray"/>
</Style>
<!-- Style for data rows. -->
<Style x:Key="dataRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontSize" Value="12"/>
</Style>
<!-- Style for data cells. -->
<Style TargetType="{x:Type TableCell}">
<Setter Property="Padding" Value="0.1cm"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="0.01cm"/>
</Style>
</Table.Resources>
<Table.Columns>
<TableColumn Width="0.8*" />
<TableColumn Width="0.2*" />
</Table.Columns>
<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>نام محصول</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>قیمت</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
<TableRowGroup Style="{StaticResource dataRowStyle}">
<xrd:TableRowForDataTable TableName="Product">
<TableCell>
<Paragraph>
<xrd:InlineTableCellValue PropertyName="Name" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<xrd:InlineTableCellValue PropertyName="Price" AggregateGroup="Group1" />
</Paragraph>
</TableCell>
</xrd:TableRowForDataTable>
</TableRowGroup>
<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Right">
<Bold>جمع کل</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" />
</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
<Paragraph TextAlignment="Center" Margin="5">
در این گزارش
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Count"
EmptyValue="هیچ"
FontWeight="Bold" /> محصول با جمع کل قیمت
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" /> وجود دارند.
</Paragraph>
</xrd:SectionDataGroup>
</Section>
در ابتدا توسط دو پاراگراف، عنوان گزارش و یک سطر زیر آن نمایش داده شدهاند. بدیهی است هر نوع شیء و فرمت مجاز در FlowDocument را میتوان در این قسمت نیز قرار داد.
سپس یک SectionDataGroup جهت نمایش لیست آیتمها اضافه شده و داخل آن یک جدول که بیانگر ساختار جدول نمایش رکوردهای گزارش میباشد، ایجاد گردیده است.
سه TableRowGroup در این جدول تعریف شدهاند.
TableRowGroup های اولی و آخری دو سطر اول و آخر جدول گزارش را مشخص میکنند (سطر عناوین ستونها در ابتدا و سطر جمع کل در پایان گزارش)
از TableRowGroup میانی برای نمایش رکوردهای مرتبط با نام جدول مورد گزارشگیری استفاده شده است. توسط TableRowForDataTable آن نام این جدول باید مشخص شود که در اینجا همان نام کلاس مدل برنامه است. به کمک InlineTableCellValue، خاصیتهایی از این کلاس را که نیاز است در گزارش حضور داشته باشند، ذکر خواهیم کرد. نکتهی مهم آن AggregateGroup ذکر شده است. توسط آن میتوان اعمال جمع، محاسبه تعداد، حداقل و حداکثر و امثال آنرا که در فایل InlineAggregateValue.cs سورس کتابخانه ذکر شدهاند، به فیلدهای مورد نظر اعمال کرد. برای مثال میخواهیم جمع کل قیمت را در پایان گزارش نمایش دهیم به همین جهت نیاز بود تا یک AggregateGroup را برای این منظور تعریف کنیم.
از این AggregateGroup در سومین TableRowGroup تعریف شده به کمک xrd:InlineAggregateValue جهت نمایش جمع نهایی استفاده شده است.
همچنین اگر نیاز بود در پایان گزارش اطلاعات بیشتری نیز نمایش داده شود به سادگی میتوان با تعریف یک پاراگراف جدید، اطلاعات مورد نظر را نمایش داد.
4) نمایش گزارش تهیه شده
نمایش این گزارش بسیار ساده است. View برنامه به صورت زیر خواهد بود:
<Window x:Class="WpfRptTests.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:CodeReason.Reports.Controls;assembly=CodeReason.Reports"
xmlns:vm="clr-namespace:WpfRptTests.ViewModel"
Title="MainWindow" WindowState="Maximized" Height="350" Width="525">
<Window.Resources>
<vm:ProductViewModel x:Key="vmProductViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmProductViewModel}}">
<c:BusyDecorator IsBusyIndicatorHidden="{Binding RptGuiModel.IsBusyIndicatorHidden}">
<DocumentViewer Document="{Binding RptGuiModel.Document}" />
</c:BusyDecorator>
</Grid>
</Window>
تعریف ابتدایی RptGuiModel به صورت زیر است (جهت مشخص سازی مقادیر IsBusyIndicatorHidden و Document در حین بایندینگ اطلاعات):
using System.ComponentModel;
using System.Windows.Documents;
namespace WpfRptTests.Model
{
public class RptGuiModel
{
public IDocumentPaginatorSource Document { get; set; }
public bool IsBusyIndicatorHidden { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using CodeReason.Reports;
using WpfRptTests.Helper;
using WpfRptTests.Model;
namespace WpfRptTests.ViewModel
{
public class ProductViewModel
{
#region Constructors (1)
public ProductViewModel()
{
RptGuiModel = new RptGuiModel();
if (Stat.IsInDesignMode) return;
//انجام عملیات نمایش گزارش در یک ترد دیگر جهت قفل نشدن ترد اصلی برنامه
showReportAsync();
}
#endregion Constructors
#region Properties (1)
public RptGuiModel RptGuiModel { set; get; }
#endregion Properties
#region Methods (3)
// Private Methods (3)
private static List<Product> getProducts()
{
var products = new List<Product>();
for (var i = 0; i < 100; i++)
products.Add(new Product { Name = string.Format("Product{0}", i), Price = i });
return products;
}
private void showReport()
{
try
{
//Show BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = false;
var reportDocument =
new ReportDocument
{
XamlData = File.ReadAllText(@"Report\SimpleReport.xaml"),
XamlImagePath = Path.Combine(Environment.CurrentDirectory, @"Report\")
};
var data = new ReportData();
// تعریف متغیرهای دلخواه و مقدار دهی آنها
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحید");
// استفاده از یک سری اطلاعات آزمایشی به عنوان منبع داده
data.DataTables.Add(getProducts().ToDataTable());
var xps = reportDocument.CreateXpsDocument(data);
//انقیاد آن به صورت غیر همزمان در ترد اصلی برنامه
DispatcherHelper.DispatchAction(
() => RptGuiModel.Document = xps.GetFixedDocumentSequence()
);
}
catch (Exception ex)
{
//وجود این مورد ضروری است زیرا بروز استثناء در یک ترد به معنای خاتمه آنی برنامه است
//todo: log errors
}
finally
{
//Hide BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = true;
}
}
private void showReportAsync()
{
var thread = new Thread(showReport);
thread.SetApartmentState(ApartmentState.STA); //for DocumentViewer
thread.Start();
}
#endregion Methods
}
}
توضیحات:
برای اینکه حین نمایش گزارش، ترد اصلی برنامه قفل نشود، از ترد استفاده شد و استفاده ترد به همراه DocumentViewer کمی نکته دار است:
- ترد تعریف شده باید از نوع STA باشد که در متد showReportAsync مشخص شده است.
- حین بایندیگ Document تولید شده توسط کتابخانهی گزارشگیری به خاصیت Document کنترل، حتما باید کل عملیات در ترد اصلی برنامه صورت گیرد که سورس کلاس DispatcherHelper را در فایل پیوست خواهید یافت.
کل عملیات این ViewModel در متد showReport رخ میدهد، ابتدا فایل گزارش بارگذاری میشود، سپس متغیرهای سفارشی مورد نظر تعریف و مقدار دهی خواهند شد. در ادامه یک سری داده آزمایشی تولید و به DataTables گزارش ساز اضافه میشوند. در پایان XPS Document متناظر آن تولید شده و به کنترل نمایشی برنامه بایند خواهد شد.
دریافت سورس این مثال
در این مطلب قصد داریم کتابخانهای با قابلیت استفادهی مجدد را جهت بکارگیری «PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده میکند» ارائه دهیم. نکات ارائه شدهی در آنرا میتوان جهت تبدیل و استفادهی از تمام DatePickerهای مشابه نیز بکاربرد.
نیازهای یک ورودی تاریخ سازگار با EditForm
- باید قابلیت استفادهی مجدد را داشته باشد. یعنی باید به صورت یک کامپوننت مجزا و یا به صورت یک کتابخانهی مجزا ارائه شود.
- باید با سیستم اعتبارسنجی EditForm یکپارچه باشد.
- باید جنریک باشد. یعنی باید بتوان در صورت نیاز DateTime ، DateTimeOffset و DateOnly و نمونههای nullable آنهارا توسط این کامپوننت دریافت کرد و ورودی و خروجی آن رشتهای نباشد.
نیاز به ارثبری از <InputBase<T جهت ارائهی کامپوننتهایی سازگار با EditForm
تقریبا تمام کامپوننتهای استاندارد EditForm ارائه شدهی توسط Blazor، از کامپوننت پایهای به نام <InputBase<T مشتق میشوند. این کلاس، یک کلاس abstract است که قابلیتهای بیشتری را نسبت به یک input سادهی HTML ای مانند اعتبارسنجی سازگار با EditForm ارائه میدهد. به همین جهت توصیه میشود تا اگر خواستید یک کامپوننت ورودی را برای استفادهی در Blazor و EditForm آن طراحی کنید، با ارثبری از این کلاس شروع کنید و صرفا کار را با یک input ساده، شروع نکنید.
برای استفادهی از آن، ابتدای کامپوننت Blazor ما به این صورت شروع خواهد شد:
که دو متد را برای بازنویسی در اختیار ما قرار میدهد:
علت وجود این دو متد، این است که مرورگرها، رشتهها را در اختیار ما قرار میدهند و ما باید راهی را برای تبدیل T به یک رشته و عکس آن را ارائه دهیم. با بازنویسی متد TryParseValueFromString، میتوان رشتهی دریافتی از کاربر را تبدیل به T کرد و اگر این تبدیل میسر نبود، با مقدار دهی validationErrorMessage، مشکل را به کاربر، با یک پیام شکست اعتبارسنجی، اعلام کرد. کار متد FormatValueAsString، تبدیل T به یک رشتهاست تا در input واقع در صفحه، نمایش داده شود. در اینجا میتوان فرمت خاصی را به شیء دریافتی اعمال و نمایش داد.
ایجاد یک کتابخانهی جدید برای محصور سازی DatePicker جاوااسکریپتی
چون قصد استفادهی مجدد از این کامپوننت جدید را در پروژههای مختلف داریم، بهتر است آنرا تبدیل به یک «کتابخانهی Blazor» کنیم. به همین جهت کتابخانهی فرضی BlazorPersianJavaScriptDatePicker.Lib را در اینجا ایجاد کردهایم.
در ابتدا دو فایل PersianDatePicker.js و PersianDatePicker.css موجود و مدنظر را در پوشههای js و css پوشهی wwwroot این کتابخانه کپی میکنیم. بنابراین استفاده کنندهی از آن، مانند پروژهی blazor wasm جدیدی به نام BlazorPersianJavaScriptDatePicker، باید ارجاعاتی را به آنها به صورت زیر اضافه کند:
همچنین در فایل Imports.razor_ آن نیز بهتر است فضای نام این کتابخانه، ذکر شود تا به سادگی بتوان از کامپوننت PersianDatePicker در آن استفاده کرد:
شروع به پیاده سازی کامپوننت PersianDatePicker
در ادامه کامپوننت جدید PersianDatePicker.razor را به پروژهی کتابخانه اضافه میکنیم. قسمت razor آن به صورت زیر است:
همانطور که مشاهده میکنید، کار با جنریک تعریف کردن و ارثبری از InputBase شروع میشود.
در اینجا با کلیک بر روی دکمهی 📅، کار فراخوانی متد PersianDatePicker.Show مربوط به datePicker جاوا اسکریپتی صورت میگیرد. همچنین هر طراحی را که در اینجا ارائه دهیم، قالب UI پیشفرض InputBase را بازنویسی میکند.
نیاز به دریافت تاریخ تنظیم شدهی توسط کدهای جاوااسکریپتی در کامپوننت Blazor
کتابخانههای جاوااسکریپتی با مقداردهی مستقیم textbox.value سبب تغییر مقدار آن میشوند. نکتهی مهم اینجا است که نه فقط Blazor این تغییرات را ردیابی نمیکند، بلکه اگر با استفاده از متد استاندارد جاوااسکریپتی addEventListener به تغییرات این input گوش فرا دهیم، هیچ رخدادی را مشاهده نخواهیم کرد. به همین جهت نیاز است اندکی کدهای PersianDatePicker.js را تغییر دهیم (و این مورد جهت تمام کتابخانههای مشابه یکسان است):
در اینجا پس از تغییر خاصیت value، باید به صورت دستی سبب بروز رخداد change شد تا addEventListenerها بتوانند این رخداد را ردیابی کنند. به همین جهت فایل مجزایی را به نام wwwroot\js\activateDatePicker.js به کتابخانهی blazor اضافه میکنیم:
هدف از این کدها این است که جهت element یا همان datePicker جاری، بتوان رخداد change را ثبت کرد و به تغییرات آن گوش فرا داد تا هر زمانیکه کدهای جاوا اسکریپتی datePicker سبب تغییری در خاصیت value شدند، بتوان آنرا به کامپوننت Blazor ارسال کرد. وهلهای از این کامپوننت توسط objectReference در اینجا دریافت شده و سپس متد OnInputFieldChanged کامپوننت را با مقدار جدید وارد شده، فراخوانی میکند.
بنابراین این فایل جدید نیز باید به index.html مصرف کننده اضافه شود:
فعالسازی DatePicker در اولین بار نمایش کامپوننت Blazor
تا اینجا زیرساخت دریافت مقدار تنظیمی توسط کاربر را در کامپوننت Blazor فراهم کردیم. اکنون نوبت به استفادهی از آن است:
- اگر دقت کرده باشید در تعاریف razor کامپوننت، "ref="ElementReference@ وجود دارد که یک ElementReference است و توسط آن میتوان در متد OnAfterRenderAsync، ارجاعی را به المان جاری، به کدهای جاوااسکریپتی متد enableDatePicker ارسال کرد.
- همچنین چون نمیخواهیم متد OnInputFieldChanged را به صورت static تعریف کنیم، نیاز است تا یک DotNetObjectReference را ایجاد و به متد enableDatePicker ارسال کرد تا توسط آن بتوان به یک instance method کلاس جاری دسترسی یافت و به سادگی مقادیر کامپوننت را تغییر داد:
- در پایان کار کامپوننت، باید این DotNetObjectReference را Dispose کرد.
نیاز به تبدیل T به تاریخ رشتهای و برعکس
زیر ساخت تبدیلات جنریک تاریخ میلادی به شمسی در کتابخانهی « DNTPersianUtils.Core » پیشبینی شدهاست و فقط کافی است از آن استفاده کنیم. با وجود این زیرساخت، تهیهی کامپوننتهای جنریک تاریخ شمسی بسیار ساده میشود:
در اینجا قسمت نهایی و تکمیلی کامپوننت محصور کنندهی DatePicker را مشاهده میکنید که بسیار سادهاست:
- InputBase به همراه یک خاصیت عمومی دوطرفهی Value است که امکان تعریفی مانند bind-Value@ را میسر میکند.
- این Value به همراه یک خاصیت متناظر رشتهای به نام CurrentValueAsString نیز هست که در اینجا از آن استفاده میکنیم و کار با آن، بایندینگ دوطرفه و همچنین اعتبارسنجی خودکار و فعالسازی متدهای بازنویسی شدهی InputBase را میسر میکند.
- پیاده سازی متدهای بازنویسی شدهی جنریک TryParseValueFromString و FormatValueAsString، با استفاده از دو متد TryParsePersianDateToDateTimeOrDateTimeOffset و FormatDateToShortPersianDate کتابخانهی « DNTPersianUtils.Core » انجام شدهاند و اصل کار تهیهی یک کامپوننت جنریک تاریخ شمسی را انجام میدهند.
استفادهی از کامپوننت Blazor تهیه شده
یک کامپوننت تاریخ شمسی باید بتواند تمام حالات و نوعهای زیر را پوشش دهد که به لطف جنریک بودن کامپوننت تهیه شده، این امر میسر است:
سپس از این کامپوننت، در صفحهی Index مثال پیوست به صورت زیر استفاده شدهاست:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorPersianJavaScriptDatePicker.zip
نیازهای یک ورودی تاریخ سازگار با EditForm
- باید قابلیت استفادهی مجدد را داشته باشد. یعنی باید به صورت یک کامپوننت مجزا و یا به صورت یک کتابخانهی مجزا ارائه شود.
- باید با سیستم اعتبارسنجی EditForm یکپارچه باشد.
- باید جنریک باشد. یعنی باید بتوان در صورت نیاز DateTime ، DateTimeOffset و DateOnly و نمونههای nullable آنهارا توسط این کامپوننت دریافت کرد و ورودی و خروجی آن رشتهای نباشد.
نیاز به ارثبری از <InputBase<T جهت ارائهی کامپوننتهایی سازگار با EditForm
تقریبا تمام کامپوننتهای استاندارد EditForm ارائه شدهی توسط Blazor، از کامپوننت پایهای به نام <InputBase<T مشتق میشوند. این کلاس، یک کلاس abstract است که قابلیتهای بیشتری را نسبت به یک input سادهی HTML ای مانند اعتبارسنجی سازگار با EditForm ارائه میدهد. به همین جهت توصیه میشود تا اگر خواستید یک کامپوننت ورودی را برای استفادهی در Blazor و EditForm آن طراحی کنید، با ارثبری از این کلاس شروع کنید و صرفا کار را با یک input ساده، شروع نکنید.
برای استفادهی از آن، ابتدای کامپوننت Blazor ما به این صورت شروع خواهد شد:
@typeparam T @inherits InputBase<T>
protected override bool TryParseValueFromString( string? value, [MaybeNullWhen(false)] out T result, [NotNullWhen(false)] out string? validationErrorMessage) { // ... } protected override string FormatValueAsString(T? value) { // ... }
ایجاد یک کتابخانهی جدید برای محصور سازی DatePicker جاوااسکریپتی
چون قصد استفادهی مجدد از این کامپوننت جدید را در پروژههای مختلف داریم، بهتر است آنرا تبدیل به یک «کتابخانهی Blazor» کنیم. به همین جهت کتابخانهی فرضی BlazorPersianJavaScriptDatePicker.Lib را در اینجا ایجاد کردهایم.
در ابتدا دو فایل PersianDatePicker.js و PersianDatePicker.css موجود و مدنظر را در پوشههای js و css پوشهی wwwroot این کتابخانه کپی میکنیم. بنابراین استفاده کنندهی از آن، مانند پروژهی blazor wasm جدیدی به نام BlazorPersianJavaScriptDatePicker، باید ارجاعاتی را به آنها به صورت زیر اضافه کند:
<link href="_content/BlazorPersianJavaScriptDatePicker.Lib/css/PersianDatePicker.css" rel="stylesheet"/> <script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/PersianDatePicker.js?v=1"></script>
@using BlazorPersianJavaScriptDatePicker.Lib
شروع به پیاده سازی کامپوننت PersianDatePicker
در ادامه کامپوننت جدید PersianDatePicker.razor را به پروژهی کتابخانه اضافه میکنیم. قسمت razor آن به صورت زیر است:
@typeparam T @inherits InputBase<T> <div> <span style="cursor:pointer" onclick="PersianDatePicker.Show(document.getElementById('@ElementId'), '@Today')"> 📅 </span> <input @attributes="@AdditionalAttributes" type="text" dir="ltr" @ref="ElementReference" name="@ElementId" id="@ElementId" autocapitalize="off" autocorrect="off" autocomplete="off" value="@EnteredValue" @oninput="OnInput"/> @if (ValueExpression is not null) { <ValidationMessage For="@ValueExpression"/> } </div>
در اینجا با کلیک بر روی دکمهی 📅، کار فراخوانی متد PersianDatePicker.Show مربوط به datePicker جاوا اسکریپتی صورت میگیرد. همچنین هر طراحی را که در اینجا ارائه دهیم، قالب UI پیشفرض InputBase را بازنویسی میکند.
نیاز به دریافت تاریخ تنظیم شدهی توسط کدهای جاوااسکریپتی در کامپوننت Blazor
کتابخانههای جاوااسکریپتی با مقداردهی مستقیم textbox.value سبب تغییر مقدار آن میشوند. نکتهی مهم اینجا است که نه فقط Blazor این تغییرات را ردیابی نمیکند، بلکه اگر با استفاده از متد استاندارد جاوااسکریپتی addEventListener به تغییرات این input گوش فرا دهیم، هیچ رخدادی را مشاهده نخواهیم کرد. به همین جهت نیاز است اندکی کدهای PersianDatePicker.js را تغییر دهیم (و این مورد جهت تمام کتابخانههای مشابه یکسان است):
function setValue(date) { _textBox.value = date; // NOTE: To notify the addEventListener('change', fn) _textBox.dispatchEvent(new Event('change')); _textBox.focus(); hide(); try { _textBox.onchange(); }catch(ex) {} }
window.activateDatePicker = { enableDatePicker: function (element, objectReference) { element.addEventListener('change', function (evt) { objectReference.invokeMethodAsync("OnInputFieldChanged", this.value); }); } };
بنابراین این فایل جدید نیز باید به index.html مصرف کننده اضافه شود:
<script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/activateDatePicker.js?v=1"></script>
فعالسازی DatePicker در اولین بار نمایش کامپوننت Blazor
تا اینجا زیرساخت دریافت مقدار تنظیمی توسط کاربر را در کامپوننت Blazor فراهم کردیم. اکنون نوبت به استفادهی از آن است:
public partial class PersianDatePicker<T> : IDisposable { private bool _isDisposed; private DotNetObjectReference<PersianDatePicker<T>>? _objectReference; private string ElementId { get; } = Guid.NewGuid().ToString("N"); private ElementReference? ElementReference { set; get; } private string Today { get; } = DateTime.Now.ToShortPersianDateString(); [Inject] private IJSRuntime JsRuntime { set; get; } = default!; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _objectReference = DotNetObjectReference.Create(this); await JsRuntime.InvokeVoidAsync("activateDatePicker.enableDatePicker", ElementReference, _objectReference); EnteredValue = CurrentValueAsString; StateHasChanged(); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!_isDisposed) { try { _objectReference?.Dispose(); } finally { _isDisposed = true; } } } }
- همچنین چون نمیخواهیم متد OnInputFieldChanged را به صورت static تعریف کنیم، نیاز است تا یک DotNetObjectReference را ایجاد و به متد enableDatePicker ارسال کرد تا توسط آن بتوان به یک instance method کلاس جاری دسترسی یافت و به سادگی مقادیر کامپوننت را تغییر داد:
[JSInvokable] public void OnInputFieldChanged(string? value)
نیاز به تبدیل T به تاریخ رشتهای و برعکس
زیر ساخت تبدیلات جنریک تاریخ میلادی به شمسی در کتابخانهی « DNTPersianUtils.Core » پیشبینی شدهاست و فقط کافی است از آن استفاده کنیم. با وجود این زیرساخت، تهیهی کامپوننتهای جنریک تاریخ شمسی بسیار ساده میشود:
public partial class PersianDatePicker<T> : IDisposable { private string? _enteredValue; private string? EnteredValue { set => _enteredValue = value; get => UsePersianNumbers ? _enteredValue.ToPersianNumbers() : _enteredValue; } [Parameter] public bool UsePersianNumbers { set; get; } [Parameter] public string ParsingErrorMessage { get; set; } = "لطفا در ورودی {0} تاریخ شمسی معتبری را وارد نمائید."; [Parameter] public int BeginningOfCentury { set; get; } = 1400; private void OnInput(ChangeEventArgs e) { SetCurrentValue(e.Value as string); } private void SetCurrentValue(string? value) { EnteredValue = value; CurrentValueAsString = value; } [JSInvokable] public void OnInputFieldChanged(string? value) { SetCurrentValue(value); } protected override void OnInitialized() { base.OnInitialized(); SanityCheck(); } protected override bool TryParseValueFromString( string? value, [MaybeNullWhen(false)] out T result, [NotNullWhen(false)] out string? validationErrorMessage) { validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, DisplayName); if (!value.TryParsePersianDateToDateTimeOrDateTimeOffset(out result, BeginningOfCentury)) { return false; } if (result is null) { throw new InvalidOperationException(validationErrorMessage); } validationErrorMessage = null; return true; } protected override string FormatValueAsString(T? value) { return !string.IsNullOrWhiteSpace(EnteredValue) ? EnteredValue : value.FormatDateToShortPersianDate(); } private void SanityCheck() { if (!Value.IsDateTimeOrDateTimeOffsetType()) { throw new InvalidOperationException( "The `Value` type is not a supported `date` type. DateTime, DateTime?, DateTimeOffset and DateTimeOffset? are supported."); } } // ... }
- InputBase به همراه یک خاصیت عمومی دوطرفهی Value است که امکان تعریفی مانند bind-Value@ را میسر میکند.
- این Value به همراه یک خاصیت متناظر رشتهای به نام CurrentValueAsString نیز هست که در اینجا از آن استفاده میکنیم و کار با آن، بایندینگ دوطرفه و همچنین اعتبارسنجی خودکار و فعالسازی متدهای بازنویسی شدهی InputBase را میسر میکند.
- پیاده سازی متدهای بازنویسی شدهی جنریک TryParseValueFromString و FormatValueAsString، با استفاده از دو متد TryParsePersianDateToDateTimeOrDateTimeOffset و FormatDateToShortPersianDate کتابخانهی « DNTPersianUtils.Core » انجام شدهاند و اصل کار تهیهی یک کامپوننت جنریک تاریخ شمسی را انجام میدهند.
استفادهی از کامپوننت Blazor تهیه شده
یک کامپوننت تاریخ شمسی باید بتواند تمام حالات و نوعهای زیر را پوشش دهد که به لطف جنریک بودن کامپوننت تهیه شده، این امر میسر است:
using System.ComponentModel.DataAnnotations; namespace BlazorPersianJavaScriptDatePicker.ViewModels; public class InputPersianDateViewModel { [Required] public string Name { set; get; } = default!; [Required] public DateTime BirthDayGregorian { set; get; } = DateTime.Now.AddYears(-40); public DateTime? LoginAt { set; get; } = DateTime.Now.AddMinutes(-2); [Required] public DateTimeOffset LogoutAt { set; get; } public DateTimeOffset? RegisterAt { set; get; } = DateTimeOffset.Now.AddMinutes(-10); }
<EditForm Model="Model" OnValidSubmit="DoSave"> <DataAnnotationsValidator/> <div> <label>تاریخ تولد</label> <div> <PersianDatePicker @bind-Value="Model.BirthDayGregorian" UsePersianNumbers="false" /> </div> </div> <button type="submit">ارسال</button> </EditForm>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorPersianJavaScriptDatePicker.zip
Apache Cordova یک فریمورک سورس باز برای ساخت اپلیکیشنهای چند سکویی موبایل (cross platform) با استفاده از Html5 میباشد.
طی چند مقاله، با استفاده کردن از این فریمورک در VS آشنا خوهیم شد.
هدف خالقان Cordova یافتن یک راه ساده برای تولید اپلیکیشنهای چند سکویی موبایل بود که برای رسیدن به این هدف تصمیم گرفتند از تکنولوژیهای بومی (native) و تکنولوژیهای وب استفاده کنند. به این نوع از اپلیکیشنهای موبایل، Hybrid Application میگویند.
Cordova دارای قابلیتهای بومی بالایی است و مهمتر اینکه به طور طبیعی توسط مرورگرها پشتیبانی میشود. بعد از تولد Corodva، این فریمورک تبدیل شده است به بهترین روش تولید اپلیکیشنهایی که بر روی چند نوع پلتفرم کار میکنند.
پیشتر محدودیتی که وجود داشت شامل این بود که اپلیکیشنهای موبایل، به چیزهایی بیشتر از HTML و مرورگرهای وب، نیاز داشتند. برخی از این نیازها عبارتند از ارتباط متقابل وب اپلیکیشنها با دوربین یا لیست شمارههای تماس گوشی که برطرف کردن آن هم به راحتی امکان پذیر نبود.
Cordova برای مقابله با این محدودیت، مجموعهای از رابطهای برنامه کاربردی را برای توسعه قابلیتهای بومی device، مانند لیست مخاطبین، دوربین، تشخیص دهندهی تغییر جهت گوشی (accelerometer) و مانند این موارد، در نظر گرفته است.
Cordova شامل یک سری کامپوننت به شرح زیر است:
- سورس کدی برای هر Container و برنامه محلی برای هر یک از سکوهای موبایل که پشتیبانی میشوند. container، کدهای Html5 را بر روی دستگاه (Device) رندر میکند. (در مطالب بعدی در مورد این مطلب توضیح خواهم داد)
- مجموعهای از رابطهای برنامهی کاربردی که امکان دسترسی به قابلیتهای بومی دستگاه را به برنامهی وبی که درون آن در حال اجرا است، میدهند.
- مجموعهای از ابزارها برای مدیریت فرآیند ایجاد پروژه، مدیریت پلاگینها، ساخت (با استفاده از SDKهای محلی) برنامههای محلی و تست برنامه بر روی دستگاه موبایل یا شبیه ساز .
برای ساخت یک برنامهی Cordova، در واقع شما یک وب اپلیکیشن میسازید و آن را داخل Container محلی، بسته بندی میکنید. سپس تست کرده و بعد از دیباگ میتوانید اپلیکیشن را توزیع کنید.
فرآیند بسته بندی :
داخل اپلیکیشن محلی، رابط کاربری اپلیکیشن شامل یک صفحهی نمایش که خود آن چیزی نیست به غیر از یک Web View که از فضای نمایش دستگاه استفاده میکند. زمانی که برنامه آغاز به کار میکند، برنامهی وب نوشته شده، درون این web view لود میشود و کنترلهای موجود، برای تعامل کاربر با برنامهی وب، در اختیار آن قرار میگیرند. مانند تعامل کاربر با محتوا، در برنامهها ی تحت وب، لینکها، کدهای نوشته شدهی js در فایلها و یا حتی میتواند به اینترنت دسترسی داشته باشد و محتوا را از یک وب سرور تغذیه کند.
درباره Web Views
Web View جزء برنامههای بومی است که برای رندر کردن محتوای وب (به عنوان نمونه صفحه HTML) درون اپلیکیشن بومی یا صفحه نمایش استفاده میشود. در اصل Web View یک Wrapper برنامه نویسی شده قابل دسترس برای نمایش محتوای صفحات وب توکار است.
به عنوان مثال:
در اندروید با استفاده از WebView موجود در (Using andoid.webkit.WebView) , در iOS با UIWebView موجود در (Using System/Library/Framworks/UIKit.framewor) به این هدف دست پیدا میکنند. وب اپلیکیشن ما درون این Container مانند سایر وب اپلیکیشنهایی است که هر روز با آنها سرو کار دارید و آنها را در مرورگر موبایل خود اجرا میکنید و میتوانید بین صفحات Navigation داشته باشید. وب اپلیکیشنهای معمول باید روی یک سرور هاست شوند. در برنامه نویسی چند سکویی با Cordova، این کار میتواند درون Cordova Application انجام گیرد.
شاید سؤالی در ذهن شما وجود داشته باشد که مرورگر معمولا به اپلیکیشنهای موجود در دستگاه، سخت افزار و یا APIهای بومی دستگاه، دسترسی ندارد. برای مثال شاید بگویید که یک وب اپلیکیشن معمولا به لیست مخاطبین با دوربین دستگاه و ... دسترسی ندارد.
جواب : در واقع امکان دسترسی به این قابلیتها توسط اپلیکیشن بومی (native mobile application) ایجاد میشود.
Cordova مجموعه ای از APIهای جاوااسکریپت را به عنوان اهرم اجازه برای دسترسی برنامه وب درون cordova container به قابلیتهای بومی دستگاه، در اختیار توسعه دهندگان قرار داده است.
این APIها در دو بخش پیاده سازی میشوند:
1-کتابخانهی جاوااسکریپت که اجازهی استفاده از قابلیتهای بومی را به وب اپلیکیشن میدهد و کد بومی مشابه در Container اجرا میشود که مربوط است به بخش بومی این API ها. در اصل یک کتابخانهی جاوا اسکریپت وجود دارد، اما بخش بومی APIها وابسته به سکوی (platform) انتخاب شده پیاده سازی میشود.
اگر شما از APIهای موجود استفاده نکنید، میتوانید آنها را از کتابخانه جاوااسکریپت و native container حذف کنید. این کار به صورت دستی شاید خوشایند نباشد ولی چون در Cordova 3.0 همهی API ها از بیرون وارد میشوند، میتوانید با استفاده از بحث مدیریت پلاگین آن، پلاگینها را اضافه یا حذف کنید. در بخشهای بعد با مثالهایی عملی این مباحث را کار خواهیم کرد.
ادامه دارد..
مطالب
NHibernate 3.2
نگارش نهایی NHibernate 3.2 مدتی است که ارائه شده و به همراه آن قابلیتهایی همانند Fluent NHibernate جهت حذف فایلهای XML ایی تعریف نگاشتها به کمک کد نویسی هم وجود دارد. در حال حاضر آنچنان مطالب خودآموز قابل توجهی را در این مورد نمیتوان یافت ولی در کل دو ویدیوی مقدماتی زیر میتوانند کمک خوبی جهت شروع به کار با این امکان جدید باشند:
ماخذ
در مورد حذف منطقی در EF 6x، پیشتر مطالبی را در این سایت مطالعه کردهاید:
- «پیاده سازی حذف منطقی در Entity framework» حذف منطقی، یکی از الگوهای بسیار پرکاربرد در برنامههای تجاری است. توسط آن بجای حذف فیزیکی اطلاعات، آنها را تنها به عنوان رکوردی حذف شده، «علامتگذاری» میکنیم. مزایای آن نیز به شرح زیر هستند:
- داشتن سابقهی حذف اطلاعات
- جلوگیری از cascade delete
- امکان بازیابی رکوردها و امکان ایجاد قسمتی به نام recycle bin در برنامه (شبیه به recycle bin در ویندوز که امکان بازیابی موارد حذف شده را میدهد)
- امکان داشتن رکوردهایی که در یک برنامه (به ظاهر) حذف شدهاند، اما هنوز در برنامهی دیگری در حال استفاده هستند.
- بالابردن میزان امنیت برنامه. فرض کنید سایت شما هک شده و شخصی، دسترسی به پنل مدیریتی و سطوح دسترسی مدیریتی برنامه را پیدا کردهاست. در این حالت حذف تمام رکوردهای سایت توسط او، تنها به معنای تغییر یک بیت، از یک به صفر است و بازگرداندن این درجه از خسارت، تنها با روشن کردن این بیت، برطرف میشود.
پیاده سازی حذف منطقی در EF Core شامل مراحل خاصی است که در این مطلب، جزئیات آنها را بررسی خواهیم کرد.
نیاز به تعریف دو خاصیت جدید در هر جدول
هر جدولی که قرار است soft delete به آن اعمال شود، باید دارای دو فیلد جدید bool IsDeleted و DateTime? DeletedAt باشد. میتوان این خواص را به هر موجودیتی به صورت دستی اضافه کرد و یا میتوان ابتدا یک کلاس پایهی abstract را برای آن ایجاد کرد:
و سپس موجودیتهایی را که قرار است از soft delete پشتیبانی کنند، توسط آن علامتگذاری کرد؛ مانند موجودیت Blog:
که هر بلاگ از تعدادی مطلب تشکیل شدهاست:
مزیت علامتگذاری این کلاسها، امکان کوئری گرفتن از آنها نیز میباشد که در ادامه از آن استفاده خواهیم کرد.
حذف خودکار رکوردهایی که Soft Delete شدهاند، از نتیجهی کوئریها و گزارشات
تا اینجا فقط دو خاصیت ساده را به کلاسهای مدنظر خود اضافه کردهایم. پس از آن یا میتوان در هر جائی برای مثال شرط context.Blogs.Where(blog => !blog.IsDeleted) را به صورت دستی اعمال کرد و در گزارشات، رکوردهای حذف منطقی شده را نمایش نداد و یا از زمان ارائهی EF Core 2x میتوان برای آنها Query Filter تعریف کرد. برای مثال میتوان به تنظیمات موجودیت Blog و یا Post مراجعه نمود و با استفاده از متد HasQueryFilter، همان شرط blog => !blog.IsDeleted را به صورت سراسری به تمام کوئریهای مرتبط با این موجودیتها اعمال کرد:
از این پس ذکر context.Blogs دقیقا معنای context.Blogs.Where(blog => !blog.IsDeleted) را میدهد و دیگر نیازی به ذکر صریح شرط متناظر با soft delete نیست.
در این حالت کوئریهای نهایی به صورت خودکار دارای شرط زیر خواهند شد:
اعمال خودکار QueryFilter مخصوص Soft Delete به تمام موجودیتها
همانطور که عنوان شد، مزیت علامتگذاری موجودیتها با کلاس پایهی BaseEntity، امکان کوئری گرفتن از آنها است:
در اینجا در ابتدا تمام موجودیتهایی که از BaseEntity ارث بری کردهاند، یافت میشوند. سپس بر روی آنها قرار است متد SetQueryFilter فراخوانی شود. این متد بر اساس تعاریف EF Core، یک LambdaExpression کلی را قبول میکند که نمونهی آن در متد getSoftDeleteFilter تعریف شده و سپس توسط متد addSoftDeleteQueryFilter به صورت پویا به modelBuilder اعمال میشود.
محل اعمال آن نیز در انتهای متد OnModelCreating است تا به صورت خودکار به تمام موجودیتهای موجود اعمال شود:
مشکل! هنوز هم حذف فیزیکی رخ میدهد!
تنظیمات فوق، تنها بر روی کوئریهای نوشته شده تاثیر دارند؛ اما هیچگونه تاثیری را بر روی متد Remove و سپس SaveChanges نداشته و در این حالت، هنوز هم حذف واقعی و فیزیکی رخ میدهد.
برای رفع این مشکل باید به EF Core گفت، هر چند دستور حذف صادر شده، اما آنرا تبدیل به دستور Update کن؛ یعنی فیلد IsDelete را به 1 و فیلد DeletedAt را با زمان جاری مقدار دهی کن:
در اینجا با استفاده از سیستم tracking، رکوردهای حذف شدهی با وضعیت EntityState.Deleted، به وضعیت EntityState.Unchanged تغییر پیدا میکنند، تا دیگر حذف نشوند. اما در ادامه چون دو خاصیت IsDeleted و DeletedAt این موجودیت، ویرایش میشوند، وضعیت جدید Modified خواهد بود که به کوئریهای Update تفسیر میشوند. به این ترتیب میتوان همانند قبل یک رکورد را حذف کرد:
اما دستوری که توسط EF Core صادر میشود، یک Update است:
محل اعمال متد SetAuditableEntityOnBeforeSaveChanges فوق، پیش از فراخوانی SaveChanges و به صورت زیر است:
مشکل! رکوردهای وابسته حذف نمیشوند!
حالت پیشفرض حذف رکوردها در EFCore به cascade delete تنظیم شدهاست. یعنی اگر blog با id=1 حذف شود، نه فقط این blog، بلکه تمام مطالب وابستهی به آن نیز حذف خواهند شد. اما در اینجا اگر این بلاگ را حذف کنیم:
تنها تک رکورد متناظر با آن حذف منطقی شده و مطالب متناظر با آن خیر. برای رفع این مشکل باید به صورت زیر عمل کرد:
ابتدا باید رکوردهای وابسته را توسط یک Include به حافظه وارد کرد و سپس دستور Delete را بر روی کل آن صادر نمود که یک چنین خروجی را تولید میکند:
ابتدا اولین بلاگ را حذف منطقی کرده؛ سپس تمام مطالب متناظر با آنرا که پیشتر حذف منطقی نشدهاند، یکی یکی به صورت حذف شده، علامتگذاری میکند. به این ترتیب cascade delete منطقی نیز در اینجا میسر میشود.
یک نکته: مشکل حذف منطقی و رکوردهای منحصربفرد
فرض کنید در جدولی، فیلد نام کاربری را به عنوان یک فیلد منحصربفرد تعریف کردهاید و اکنون رکوردی در این بین، حذف منطقی شدهاست. مشکلی که در آینده بروز خواهد کرد، عدم امکان ثبت رکورد جدیدی با همان نام کاربری است که حذف منطقی شدهاست؛ چون یک unique index بر روی آن وجود دارد. در این حالت اگر از SQL Server استفاده میکنید، از قابلیتی به نام filtered indexes پشتیبانی میکند که در آن امکان تعریف یک شرط و predicate، در حین تعریف ایندکسها وجود دارد. در این حالت میتوان رکوردهای حذف منطقی شده را به ایندکس وارد نکرد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: EFCoreSoftDelete.zip
- «پیاده سازی حذف منطقی در Entity framework» حذف منطقی، یکی از الگوهای بسیار پرکاربرد در برنامههای تجاری است. توسط آن بجای حذف فیزیکی اطلاعات، آنها را تنها به عنوان رکوردی حذف شده، «علامتگذاری» میکنیم. مزایای آن نیز به شرح زیر هستند:
- داشتن سابقهی حذف اطلاعات
- جلوگیری از cascade delete
- امکان بازیابی رکوردها و امکان ایجاد قسمتی به نام recycle bin در برنامه (شبیه به recycle bin در ویندوز که امکان بازیابی موارد حذف شده را میدهد)
- امکان داشتن رکوردهایی که در یک برنامه (به ظاهر) حذف شدهاند، اما هنوز در برنامهی دیگری در حال استفاده هستند.
- بالابردن میزان امنیت برنامه. فرض کنید سایت شما هک شده و شخصی، دسترسی به پنل مدیریتی و سطوح دسترسی مدیریتی برنامه را پیدا کردهاست. در این حالت حذف تمام رکوردهای سایت توسط او، تنها به معنای تغییر یک بیت، از یک به صفر است و بازگرداندن این درجه از خسارت، تنها با روشن کردن این بیت، برطرف میشود.
پیاده سازی حذف منطقی در EF Core شامل مراحل خاصی است که در این مطلب، جزئیات آنها را بررسی خواهیم کرد.
نیاز به تعریف دو خاصیت جدید در هر جدول
هر جدولی که قرار است soft delete به آن اعمال شود، باید دارای دو فیلد جدید bool IsDeleted و DateTime? DeletedAt باشد. میتوان این خواص را به هر موجودیتی به صورت دستی اضافه کرد و یا میتوان ابتدا یک کلاس پایهی abstract را برای آن ایجاد کرد:
using System; namespace EFCoreSoftDelete.Entities { public abstract class BaseEntity { public int Id { get; set; } public bool IsDeleted { set; get; } public DateTime? DeletedAt { set; get; } } }
using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace EFCoreSoftDelete.Entities { public class Blog : BaseEntity { public string Name { set; get; } public virtual ICollection<Post> Posts { set; get; } } public class BlogConfiguration : IEntityTypeConfiguration<Blog> { public void Configure(EntityTypeBuilder<Blog> builder) { builder.Property(blog => blog.Name).HasMaxLength(450).IsRequired(); builder.HasIndex(blog => blog.Name).IsUnique(); builder.HasData(new Blog { Id = 1, Name = "Blog 1" }); builder.HasData(new Blog { Id = 2, Name = "Blog 2" }); builder.HasData(new Blog { Id = 3, Name = "Blog 3" }); } } }
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace EFCoreSoftDelete.Entities { public class Post : BaseEntity { public string Title { set; get; } public Blog Blog { set; get; } public int BlogId { set; get; } } public class PostConfiguration : IEntityTypeConfiguration<Post> { public void Configure(EntityTypeBuilder<Post> builder) { builder.Property(post => post.Title).HasMaxLength(450); builder.HasOne(post => post.Blog).WithMany(blog => blog.Posts).HasForeignKey(post => post.BlogId); builder.HasData(new Post { Id = 1, BlogId = 1, Title = "Post 1" }); builder.HasData(new Post { Id = 2, BlogId = 1, Title = "Post 2" }); builder.HasData(new Post { Id = 3, BlogId = 1, Title = "Post 3" }); builder.HasData(new Post { Id = 4, BlogId = 1, Title = "Post 4" }); builder.HasData(new Post { Id = 5, BlogId = 2, Title = "Post 5" }); } } }
حذف خودکار رکوردهایی که Soft Delete شدهاند، از نتیجهی کوئریها و گزارشات
تا اینجا فقط دو خاصیت ساده را به کلاسهای مدنظر خود اضافه کردهایم. پس از آن یا میتوان در هر جائی برای مثال شرط context.Blogs.Where(blog => !blog.IsDeleted) را به صورت دستی اعمال کرد و در گزارشات، رکوردهای حذف منطقی شده را نمایش نداد و یا از زمان ارائهی EF Core 2x میتوان برای آنها Query Filter تعریف کرد. برای مثال میتوان به تنظیمات موجودیت Blog و یا Post مراجعه نمود و با استفاده از متد HasQueryFilter، همان شرط blog => !blog.IsDeleted را به صورت سراسری به تمام کوئریهای مرتبط با این موجودیتها اعمال کرد:
public class BlogConfiguration : IEntityTypeConfiguration<Blog> { public void Configure(EntityTypeBuilder<Blog> builder) { // ... builder.HasQueryFilter(blog => !blog.IsDeleted); } }
در این حالت کوئریهای نهایی به صورت خودکار دارای شرط زیر خواهند شد:
SELECT [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[IsDeleted] <> CAST(1 AS bit)
اعمال خودکار QueryFilter مخصوص Soft Delete به تمام موجودیتها
همانطور که عنوان شد، مزیت علامتگذاری موجودیتها با کلاس پایهی BaseEntity، امکان کوئری گرفتن از آنها است:
namespace EFCoreSoftDelete.DataLayer { public static class GlobalFiltersManager { public static void ApplySoftDeleteQueryFilters(this ModelBuilder modelBuilder) { foreach (var entityType in modelBuilder.Model .GetEntityTypes() .Where(eType => typeof(BaseEntity).IsAssignableFrom(eType.ClrType))) { entityType.addSoftDeleteQueryFilter(); } } private static void addSoftDeleteQueryFilter(this IMutableEntityType entityData) { var methodToCall = typeof(GlobalFiltersManager) .GetMethod(nameof(getSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static) .MakeGenericMethod(entityData.ClrType); var filter = methodToCall.Invoke(null, new object[] { }); entityData.SetQueryFilter((LambdaExpression)filter); } private static LambdaExpression getSoftDeleteFilter<TEntity>() where TEntity : BaseEntity { return (Expression<Func<TEntity, bool>>)(entity => !entity.IsDeleted); } } }
محل اعمال آن نیز در انتهای متد OnModelCreating است تا به صورت خودکار به تمام موجودیتهای موجود اعمال شود:
namespace EFCoreSoftDelete.DataLayer { public class ApplicationDbContext : DbContext { //... protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(typeof(BaseEntity).Assembly); modelBuilder.ApplySoftDeleteQueryFilters(); }
مشکل! هنوز هم حذف فیزیکی رخ میدهد!
تنظیمات فوق، تنها بر روی کوئریهای نوشته شده تاثیر دارند؛ اما هیچگونه تاثیری را بر روی متد Remove و سپس SaveChanges نداشته و در این حالت، هنوز هم حذف واقعی و فیزیکی رخ میدهد.
برای رفع این مشکل باید به EF Core گفت، هر چند دستور حذف صادر شده، اما آنرا تبدیل به دستور Update کن؛ یعنی فیلد IsDelete را به 1 و فیلد DeletedAt را با زمان جاری مقدار دهی کن:
namespace EFCoreSoftDelete.DataLayer { public static class AuditableEntitiesManager { public static void SetAuditableEntityOnBeforeSaveChanges(this ApplicationDbContext context) { var now = DateTime.UtcNow; foreach (var entry in context.ChangeTracker.Entries<BaseEntity>()) { switch (entry.State) { case EntityState.Added: //TODO: ... break; case EntityState.Modified: //TODO: ... break; case EntityState.Deleted: entry.State = EntityState.Unchanged; //NOTE: For soft-deletes to work with the original `Remove` method. entry.Entity.IsDeleted = true; entry.Entity.DeletedAt = now; break; } } } } }
var post1 = context.Posts.Find(1); if (post1 != null) { context.Remove(post1); context.SaveChanges(); }
Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:11:32' (Nullable = true), @p1='True'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [Posts] SET [DeletedAt] = @p0, [IsDeleted] = @p1 WHERE [Id] = @p2; SELECT @@ROWCOUNT;
محل اعمال متد SetAuditableEntityOnBeforeSaveChanges فوق، پیش از فراخوانی SaveChanges و به صورت زیر است:
namespace EFCoreSoftDelete.DataLayer { public class ApplicationDbContext : DbContext { // ... public override int SaveChanges(bool acceptAllChangesOnSuccess) { ChangeTracker.DetectChanges(); beforeSaveTriggers(); ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChanges(acceptAllChangesOnSuccess); ChangeTracker.AutoDetectChangesEnabled = true; return result; } // ... private void beforeSaveTriggers() { setAuditProperties(); } private void setAuditProperties() { this.SetAuditableEntityOnBeforeSaveChanges(); } } }
حالت پیشفرض حذف رکوردها در EFCore به cascade delete تنظیم شدهاست. یعنی اگر blog با id=1 حذف شود، نه فقط این blog، بلکه تمام مطالب وابستهی به آن نیز حذف خواهند شد. اما در اینجا اگر این بلاگ را حذف کنیم:
ar blog1 = context.Blogs.FirstOrDefault(blog => blog.Id == 1); if (blog1 != null) { context.Remove(blog1); context.SaveChanges(); }
var blog1AndItsRelatedPosts = context.Blogs .Include(blog => blog.Posts) .FirstOrDefault(blog => blog.Id == 1); if (blog1AndItsRelatedPosts != null) { context.Remove(blog1AndItsRelatedPosts); context.SaveChanges(); }
SELECT [t].[Id], [t].[DeletedAt], [t].[IsDeleted], [t].[Name], [t0].[Id], [t0].[BlogId], [t0].[DeletedAt], [t0].[IsDeleted], [t0].[Title] FROM ( SELECT TOP(1) [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name] FROM [Blogs] AS [b] WHERE ([b].[IsDeleted] <> CAST(1 AS bit)) AND ([b].[Id] = 1) ) AS [t] LEFT JOIN ( SELECT [p].[Id], [p].[BlogId], [p].[DeletedAt], [p].[IsDeleted], [p].[Title] FROM [Posts] AS [p] WHERE [p].[IsDeleted] <> CAST(1 AS bit) ) AS [t0] ON [t].[Id] = [t0].[BlogId] ORDER BY [t].[Id], [t0].[Id] Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:25:00' (Nullable = true), @p1='True', @p5='2', @p3='2020-09-17T05:25:00' (Nullable = true), @p4='True', @p8='3', @p6='2020-09-17T05:25:00' (Nullable = true), @p7='True', @p11='4', @p9='2020-09-17T05:25:00' (Nullable = true), @p10='True'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [Blogs] SET [DeletedAt] = @p0, [IsDeleted] = @p1 WHERE [Id] = @p2; SELECT @@ROWCOUNT; UPDATE [Posts] SET [DeletedAt] = @p3, [IsDeleted] = @p4 WHERE [Id] = @p5; SELECT @@ROWCOUNT; UPDATE [Posts] SET [DeletedAt] = @p6, [IsDeleted] = @p7 WHERE [Id] = @p8; SELECT @@ROWCOUNT; UPDATE [Posts] SET [DeletedAt] = @p9, [IsDeleted] = @p10 WHERE [Id] = @p11; SELECT @@ROWCOUNT;
یک نکته: مشکل حذف منطقی و رکوردهای منحصربفرد
فرض کنید در جدولی، فیلد نام کاربری را به عنوان یک فیلد منحصربفرد تعریف کردهاید و اکنون رکوردی در این بین، حذف منطقی شدهاست. مشکلی که در آینده بروز خواهد کرد، عدم امکان ثبت رکورد جدیدی با همان نام کاربری است که حذف منطقی شدهاست؛ چون یک unique index بر روی آن وجود دارد. در این حالت اگر از SQL Server استفاده میکنید، از قابلیتی به نام filtered indexes پشتیبانی میکند که در آن امکان تعریف یک شرط و predicate، در حین تعریف ایندکسها وجود دارد. در این حالت میتوان رکوردهای حذف منطقی شده را به ایندکس وارد نکرد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: EFCoreSoftDelete.zip
نظرات مطالب
آپلود فایل توسط فرمهای پویای jqGrid
«Content type مقدار بازگشتی از متد UploadFiles حتما باید text/html باشد (افزونهی ارسال فایلها، اینگونه کار میکند).»
توسط همان فایرباگ بررسی کنید که چه خروجی از سمت سرور به سمت کاربر ارسال شده (در برگهی response آدرس درخواستی)؟ چه فرمتی دارد؟ شامل چه اطلاعاتی است؟
return Json(new { FileName = product.ImageName }, "text/html", JsonRequestBehavior.AllowGet);
علت این است که html ایی که در فایرباگ بررسی میشه عموما به دلیل یک سری از نرمال سازیها توسط موتور فایرفاکس و همچنین خودش، با html اصلی یک سایت متفاوت است. به همین جهت XPath استخراجی از آن روی سایت اصلی کار نخواهد کرد.
یک برنامه کمکی برای یافتن XPathها به همان نحوی که هستند.
یک برنامه کمکی برای یافتن XPathها به همان نحوی که هستند.
در قسمت قبل توضیحاتی در مورد تب HTML ارائه کردیم.
Panel
Context Menu
این منو زمانی که در پنل راست کلیک کنید ظاهر میشود و نسبت به منطقه (Context)ای که در آن راست کلیک کرده اید ، گزینههای متفاوتی را مشاهده خواهید کرد. در جدول زیر ، گزینهها ، Contextشان و توضیح هر گزینه آمده است.
2 - Computed
دراین تب نتیجهی پردازش استایلهای ارائه شده توسط کاربر ، برای تگ مشخص شده در قسمت NodeView نمایش داده میشود. (مقادیر استایل هایی که در نهایت بروی تگ اعمال شده اند.)
Style Tracing
برای ردیابی استایلها ، استایلها به ترتیب اعمال شدنشان مرتب شده اند و اولین مقدار ، مقداری است که اعمال شده است.
مقادیر Overwrite بصورت خط کشیده شده و استایلهای Overwrite شده بصورت خاکستری-کمرنگ نمایش داده میشوند.
هر استایل هم مانند تب Style ، یک لینک به منبع خود دارد.
Options Menu
Context Menu
این منو زمانی که در پنل راست کلیک کنید ظاهر میشود و نسبت به منطقه (Context)ای که در آن راست کلیک کرده اید ، گزینههای متفاوتی را مشاهده خواهید کرد. در جدول زیر ، گزینهها ، Contextشان و توضیح هر گزینه آمده است.
3 - Layout
در این تب ، مقادیر Box Model بصورت بصری نمایش میدهد. میتوان با کلیک کردن بروی هریک از مقادیر ، آن را ویرایش کرد. (این تغییر بصورت inline در تگ اعمال میشود.)
با حرکت موس بروی قسمتهای مختلف ، میتوان همان قسمتها را در صفحه بصورت خط کشی شده مشاهده کرد.
(البته ظاهرا در ورژن 1.10.4 که بنده استفاده میکنم ، عملیات ویرایش مقادیر به درستی انجام نمیشود.)
Options Menu
4 - DOM
این پنل اطلاعات DOM تگ جاری را نمایش میدهد.
این پنل تمام قابلیتهای پنل DOM اصلی را دارا میباشد.
(در مقالات آینده با تب DOM آشنا خواهیم شد.)
در این قسمت توضیحات کاملی در مورد پنلهای جانبیِ داخل پنل HTML میدهیم.
Side Panels
در پنل HTML درکنار ارائه امکاناتی برای مشاهده و کار با تگهای صفحه ، اطلاعات و امکانات دیگری هم برای تگ انتخاب شده در قسمت NodeView وجود دارد.
Side Panels
در پنل HTML درکنار ارائه امکاناتی برای مشاهده و کار با تگهای صفحه ، اطلاعات و امکانات دیگری هم برای تگ انتخاب شده در قسمت NodeView وجود دارد.
این امکانات در پنل هایی که سمت راست پنل اصلی قرار دارند گنجانده شده است که به ترتیب برای نمایش و ویرایش استایلها ، مشاهده استایلهای محاسبه شده ، مشاهده Layout یا آرایش و نمایش اطلاعات DOM تگ انتخاب شده در NodeView هستند.
1 - Style
در این تب استایل هایی که در حال حاظر بروی تگ انتخاب شده اعمال شده اند ، نمایش داده میشود.
در صورتی که موس را بروی مقادیر استایل هایی که جلوهی بصری دارند بگیرید ، یک پاپآپ کوچک نمایان میشود که مقدار را نمایش میدهد.
Options Menu
هر تب یا پنل در فایرباگ دارای یک سری تنظیمات است که Options Menu نام دارد. تب Style هم دارای یک سری تنظیمات است که دانشتن آنها بسیار به شما کمک خواهد کرد.
این منو با کلیک کردن بروی فلش تب () یا راست کلیک کردن بروی تب ظاهر میشود.
در این تب استایل هایی که در حال حاظر بروی تگ انتخاب شده اعمال شده اند ، نمایش داده میشود.
در صورتی که موس را بروی مقادیر استایل هایی که جلوهی بصری دارند بگیرید ، یک پاپآپ کوچک نمایان میشود که مقدار را نمایش میدهد.
Options Menu
هر تب یا پنل در فایرباگ دارای یک سری تنظیمات است که Options Menu نام دارد. تب Style هم دارای یک سری تنظیمات است که دانشتن آنها بسیار به شما کمک خواهد کرد.
این منو با کلیک کردن بروی فلش تب () یا راست کلیک کردن بروی تب ظاهر میشود.
- Only Show Applied Styles
در صورت انتخاب ، فقط استایل هایی که اعمال شده اند نمایش داده میشوند. (استایلهای Overwrite شده نمایش داده نمیشوند.)
(این گزینه قابلیت خوبی است ، اما چندبار برای بنده پیش آمده که این مورد به اشتباه استایلی که اعمال شده بود را هم Overwrite شده در نظر گرفته بود. پس در هین طراحی استایل و کار با CSS اگر احیانا یکی از استایل هایتان وجود نداشت و از وجود آن اطمینان داشتید ، غیرفعال کردن این گزینه را امتحان کنید.)
- Show User Agent CSS
با فعال کردن این گزینه ، استایل هایی که توسط مرورگر اعمال شده اند هم نمایش داده میشوند.
- Expand Shorthand Properties
با فعال کردن این گزینه ، استایل هایی که بصورت کوتاه شده تعریف شده اند را بصورت گسترده و باز شده نمایش میدهد.
برای مثال ، دستور margin را بصورت margin-top , margin-right , margin-bottom , margin-left نمایش میدهد.
- سه گزینه ی Colors As Hex ، Colors As RGB و Colors As HSL تعیین کنندهی فرمت نمایش رنگها هستند.
- سه گزینه ی :active ، :hover و :focus هم برای تعیین کلاس کاذب برای تگ جاری کاربرد دارند.
برای مثال شما میخواهید استایلی که یک لینک زمان موس برویش قرار دارد را بررسی کنید ، لینک را در NodeView انتخاب میکنید و سپس از گزینهی :hover را فعال میکنید.
Panel
- Element styles
استایل هایی که بصورت inline (در خود تگ) تعریف شده اند هم در این قسمت نمایش داده میشود و نام rule آن element.style است.
- Source Links
در بالا-راست هر بخش ، یک لینک قرار دارد که لینک فایل استایلی است که در همان قسمت وجود دارد و عددی که در پرانتز قرار دارد ، شماره خط استایل در همان فایل است.
اگر نام فایل با نام صفحهی جاری برابر باشد ، به معنی وجود استایل در تگ <style> در صفحهی جاری است و شمارهی بعد از # هم ایندکس تگ <style> است.
(با کلیک بروی لینک فایل ، فایل در خط مورد نظر در پنل CSS نمایش داده میشود.)
- Inherited rules
ruleهای به ارث رسیده هم در قسمتهای جداگانه به همراه استایلهای به ارث رسیده نمایش داده میشود و تگی والد که استایلها از آن به ارث رسیده اند هم در قسمت عنوان همان استایلها نمایش قرار داده شده است. (با کلیک بروی آن ، در قسمت Nodeview انتخاب میشود.)
- User agent rules
استایل هایی که توسط مرورگر اعمال شده اند (User agent rules) ، با عبارت <System> در زیر لینک منبع استایل ، مشخص شده اند.
- Overwritten styles
استایلهای overwrite شده ، با یک خط برویشان مشخص شده اند.
- Inline editing
استایلهای نمایش داده شده در این پنل را براحتی و با کلیک کردن بروی نام یا مقدار هر یک از دستورات میتوانید تغییر دهید.
برای نوشتن دستورات و مقادیر آنها میتوانید از پیشنهادهای فایرباگ هم کمک بگیرید و با دکمههای Arrow Up و Arrow Down هم بین مقادیر مجاز حرکت کنید.
دستورات یا مقادیر نا صحیح در هین تایپ ، با رنگ قرمز و مقادیر صحیح با رنگ سبز مشخص میشوند.
(این امکان خیلی مفید است ، برای مثال میخواهید فونتهای مختلف را برای یک استایل امتحان کنید ، دستور font-family را مینویسید و بعد از زدن Enter ، با دکمه های Arrow Up و Arrow Down در لحظه نتیجهی اعمال فونتهای مختلف و دردسترس را مشاهده میکنید و بهترین را بر میگزینید.
یا برای یافتن بهترین مقدار margin ، بعد از دستور margin ، زدن کلید Enter ، وارد کردن یک عدد برای شروع ، میتوان باز هم با دکمه های Arrow Up و Arrow Down به سرعت تغییر را در صفحه مشاهده کرد.)
- Rendered font highlighted
برای دستور font ، فایرباگ هوشمندانه عمل کرده و فونتی که در حال استفاده است را پررنگ میکند.
این امکان برای یافتن خطاهای متداول هنگام تعریف فونتهای غیر سیستمی ، بسیار مفید است.
Context Menu
این منو زمانی که در پنل راست کلیک کنید ظاهر میشود و نسبت به منطقه (Context)ای که در آن راست کلیک کرده اید ، گزینههای متفاوتی را مشاهده خواهید کرد. در جدول زیر ، گزینهها ، Contextشان و توضیح هر گزینه آمده است.
گزینه | Context | توضیحات |
Copy Rule Declaration | CSS selector | CSS Rule فعلی را به همراه استایل هایش در clipboard کپی میکند |
Copy Style Declaration | CSS selector | استایلهای CSS Rule فعلی را در clipboard کپی میکند |
Copy Location | source link | آدرس فایل تعریف CSS Rule را در clipboard کپی میکند |
Open in New Tab | source link | آدرس فایل تعریف CSS Rule را در تب جدید باز میکند |
Edit Element Style... | everywhere | امکان تعریف استایلهای درون تگ (inline) را محیا میکند |
Add Rule... | everywhere | یک Rule جدید ایجاد میکند CSS Rule هایی که در حال حاظر وجود دارد را هم پیشنهاد میدهد |
Delete "<rule name>" | CSS selector | CSS Rule فعلی را حذف میکند |
New Property... | CSS rule | یک استایل جدید به CSS Rule فعلی اضافه میکند |
Edit "<property name>"... | CSS property | Property فعلی را به حالت ویرایش میبرد راه دیگر ویرایش Property ، کلیک بروی آن است |
Delete "<property name>" | CSS property | Property فعلی را حذف میکند |
Disable "<property name>" | CSS property | Property فعلی را غیر فعال میکند را سریعتر ، کلیک کردن در ناحیهی پشت Property ، بروی علامت قرمز رنگ است |
Refresh | everywhere | محتویات پنل را بروز میکند |
Inspect in DOM Panel |
CSS rule | CSS Rule فعلی را در پنل DOM برای بررسی باز میکند |
Inspect in CSS Panel | CSS rule | CSS Rule فعلی را در پنل CSS برای بررسی باز میکند |
<Default Editor Name> | CSS rule | فایل تعریف استایلها را در ادیتور تعریف شده باز میکند (این گزینه در صورت تعریف ادیتور در تنظیمات FireBug نمایش داده خواهد شد) |
2 - Computed
دراین تب نتیجهی پردازش استایلهای ارائه شده توسط کاربر ، برای تگ مشخص شده در قسمت NodeView نمایش داده میشود. (مقادیر استایل هایی که در نهایت بروی تگ اعمال شده اند.)
Style Tracing
برای ردیابی استایلها ، استایلها به ترتیب اعمال شدنشان مرتب شده اند و اولین مقدار ، مقداری است که اعمال شده است.
مقادیر Overwrite بصورت خط کشیده شده و استایلهای Overwrite شده بصورت خاکستری-کمرنگ نمایش داده میشوند.
هر استایل هم مانند تب Style ، یک لینک به منبع خود دارد.
Options Menu
- Show User Agent CSS
در صورت انتخاب ، فقط استایل هایی که اعمال شده اند نمایش داده میشوند.
- Sort alphabetically
در صورت انتخاب ، استایلها به ترتیب الفبا ، و درصورت عدم انتخاب بصورت گروه بندی نمایش داده میشوند.
- Show Mozilla Specific Styles
در صورت انتخاب ، استایلهای مخصوص Mozilla را نمایش میدهد. (استایل هایی با پیشوند -moz-)
- سه گزینهی Colors As Hex ، Colors As RGB و Colors As HSL تعیین کنندهی فرمت نمایش رنگها هستند.
Context Menu
این منو زمانی که در پنل راست کلیک کنید ظاهر میشود و نسبت به منطقه (Context)ای که در آن راست کلیک کرده اید ، گزینههای متفاوتی را مشاهده خواهید کرد. در جدول زیر ، گزینهها ، Contextشان و توضیح هر گزینه آمده است.
گزینه | Context | توضیحات |
Expand All Styles | everywhere | CSS Rule فعلی را به همراه استایل هایش در clipboard کپی میکند |
Collapse All Styles | everywhere | استایلهای CSS Rule فعلی را در clipboard کپی میکند |
Inspect in DOM panel | styles | آدرس فایل تعریف CSS Rule را در تب جدید باز میکند |
Copy Location |
style source link | امکان تعریف استایلهای درون تگ (inline) را محیا میکند |
Open in New Tab | style source link | یک Rule جدید ایجاد میکند CSS Rule هایی که در حال حاظر وجود دارد را هم پیشنهاد میدهد |
Inspect in CSS panel | style source link | CSS Rule فعلی را حذف میکند |
<Default Editor Name> | style source link | فایل تعریف استایلها را در ادیتور تعریف شده باز میکند (این گزینه در صورت تعریف ادیتور در تنظیمات FireBug نمایش داده خواهد شد) |
3 - Layout
در این تب ، مقادیر Box Model بصورت بصری نمایش میدهد. میتوان با کلیک کردن بروی هریک از مقادیر ، آن را ویرایش کرد. (این تغییر بصورت inline در تگ اعمال میشود.)
با حرکت موس بروی قسمتهای مختلف ، میتوان همان قسمتها را در صفحه بصورت خط کشی شده مشاهده کرد.
(البته ظاهرا در ورژن 1.10.4 که بنده استفاده میکنم ، عملیات ویرایش مقادیر به درستی انجام نمیشود.)
Options Menu
- Show Rulers and Guides
در صورت انتخاب ، خطهای راهنما را هنگام حرکت موس بروی اجزای Box Model در صفحه نمایش میدهد.
4 - DOM
این پنل اطلاعات DOM تگ جاری را نمایش میدهد.
این پنل تمام قابلیتهای پنل DOM اصلی را دارا میباشد.
(در مقالات آینده با تب DOM آشنا خواهیم شد.)