var styles = new StyleSheet(); styles.LoadTagStyle(HtmlTags.IMG, HtmlTags.ALIGN, HtmlTags.ALIGN_CENTER); template.Html(styles); // Using iTextSharp's limited HTML to PDF capabilities (HTMLWorker class).
آشنایی با Automapping در فریم ورک Fluent NHibernate
اگر قسمتهای قبل را دنبال کرده باشید، احتمالا به پروسه طولانی ساخت نگاشتها توجه کردهاید. با کمک فریم ورک Fluent NHibernate میتوان پروسه نگاشت domain model خود را به data model متناظر آن به صورت خودکار نیز انجام داد و قسمت عمدهای از کار به این صورت حذف خواهد شد. (این مورد یکی از تفاوتهای مهم NHibernate با نمونههای مشابهی است که مایکروسافت تا تاریخ نگارش این مقاله ارائه داده است. برای مثال در نگارشهای فعلی LINQ to SQL یا Entity framework ، اول دیتابیس مطرح است و بعد ساخت کد از روی آن، در حالیکه در اینجا ابتدا کد و طراحی سیستم مطرح است و بعد نگاشت آن به سیستم دادهای و دیتابیس)
امروز قصد داریم یک سیستم ساده ثبت خبر را از صفر با NHibernate پیاده سازی کنیم و همچنین مروری داشته باشیم بر قسمتهای قبلی.
مطابق کلاس دیاگرام فوق، این سیستم از سه کلاس خبر، کاربر ثبت کنندهی خبر و گروه خبری مربوطه تشکیل شده است.
ابتدا یک پروژه کنسول جدید را به نام NHSample2 آغاز کنید. سپس ارجاعاتی را به اسمبلیهای زیر به آن اضافه نمائید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم
سپس پوشهای را به نام Domain به این پروژه اضافه نمائید (کلیک راست روی نام پروژه در VS.Net و سپس مراجعه به منوی Add->New folder). در این پوشه تعاریف موجودیتهای برنامه را قرار خواهیم داد. سه کلاس جدید Category ، User و News را در این پوشه ایجاد نمائید. محتویات این سه کلاس به شرح زیر هستند:
namespace NHSample2.Domain
{
public class User
{
public virtual int Id { get; set; }
public virtual string UserName { get; set; }
public virtual string Password { get; set; }
}
}
namespace NHSample2.Domain
{
public class Category
{
public virtual int Id { get; set; }
public virtual string CategoryName { get; set; }
}
}
using System;
namespace NHSample2.Domain
{
public class News
{
public virtual Guid Id { get; set; }
public virtual string Subject { get; set; }
public virtual string NewsText { get; set; }
public virtual DateTime DateEntered { get; set; }
public virtual Category Category { get; set; }
public virtual User User { get; set; }
}
}
اکنون کلاس جدید Config را به برنامه اضافه نمائید:
using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
namespace NHSample2
{
class Config
{
public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly).Configure(cfg);
return cfg;
}
public static void GenerateDbScript(Configuration config, string filePath)
{
bool script = true;//فقط اسکریپت دیتابیس تولید گردد
bool export = false;//نیازی نیست بر روی دیتابیس هم اجرا شود
new SchemaExport(config).SetOutputFile(filePath).Create(script, export);
}
public static void BuildDbSchema(Configuration config)
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool drop = false;//آیا اطلاعات موجود دراپ شوند
new SchemaExport(config).Execute(script, export, drop);
}
public static void CreateSQL2008DbPlusScript(string connectionString, string filePath)
{
Configuration cfg =
GenerateMapping(
MsSqlConfiguration
.MsSql2008
.ConnectionString(connectionString)
.ShowSql()
);
GenerateDbScript(cfg, filePath);
BuildDbSchema(cfg);
}
public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType)
.Mappings(m => m.AutoMappings
.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly))
)
.BuildSessionFactory();
}
}
}
در متد GenerateMapping از قابلیت Automapping موجود در فریم ورک Fluent Nhibernate استفاده شده است (بدون نوشتن حتی یک سطر جهت تعریف این نگاشتها). این متد نوع دیتابیس مورد نظر را جهت ساخت تنظیمات خود دریافت میکند. سپس با کمک کلاس AutoPersistenceModel این فریم ورک، به صورت خودکار از اسمبلی برنامه نگاشتهای لازم را به کلاسهای موجود در پوشه Domain ما اضافه میکند (مرسوم است که این پوشه در یک پروژه Class library مجزا تعریف شود که در این برنامه جهت سهولت کار در خود برنامه قرار گرفته است). قسمت Where ذکر شده به این جهت معرفی گردیده است تا Fluent Nhibernate برای تمامی کلاسهای موجود در اسمبلی جاری، سعی در تعریف نگاشتهای لازم نکند. این نگاشتها تنها به کلاسهای موجود در پوشه دومین ما محدود شدهاند.
سه متد بعدی آن، جهت ایجاد اسکریپت دیتابیس از روی این نگاشتهای تعریف شده و سپس اجرای این اسکریپت بر روی دیتابیس جاری معرفی شده، تهیه شدهاند. برای مثال CreateSQL2008DbPlusScript یک مثال ساده از استفاده دو متد قبلی جهت ایجاد اسکریپت و دیتابیس متناظر اس کیوال سرور 2008 بر اساس نگاشتهای برنامه است.
با متد CreateSessionFactory در قسمتهای قبل آشنا شدهاید. تنها تفاوت آن در این قسمت، استفاده از کلاس AutoPersistenceModel جهت تولید خودکار نگاشتها است.
در ادامه دیتابیس متناظر با موجودیتهای برنامه را ایجاد خواهیم کرد:
using System;
namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
Config.CreateSQL2008DbPlusScript(
"Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true",
"db.sql");
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
پس از اجرای برنامه، ابتدا فایل اسکریپت دیتابیس به نام db.sql در پوشه اجرایی برنامه تشکیل خواهد شد و سپس این اسکریپت به صورت خودکار بر روی دیتابیس معرفی شده اجرا میگردد. دیتابیس دیاگرام حاصل را در شکل زیر میتوانید ملاحظه نمائید:
همچنین اسکریپت تولید شده آن، صرفنظر از عبارات drop اولیه، به صورت زیر است:
create table [Category] (
Id INT IDENTITY NOT NULL,
CategoryName NVARCHAR(255) null,
primary key (Id)
)
create table [User] (
Id INT IDENTITY NOT NULL,
UserName NVARCHAR(255) null,
Password NVARCHAR(255) null,
primary key (Id)
)
create table [News] (
Id UNIQUEIDENTIFIER not null,
Subject NVARCHAR(255) null,
NewsText NVARCHAR(255) null,
DateEntered DATETIME null,
Category_id INT null,
User_id INT null,
primary key (Id)
)
alter table [News]
add constraint FKE660F9E1C9CF79
foreign key (Category_id)
references [Category]
alter table [News]
add constraint FKE660F95C1A3C92
foreign key (User_id)
references [User]
اکنون یک سری گروه خبری، کاربر و خبر را به دیتابیس خواهیم افزود:
using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample2.Domain;
namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
//با توجه به کلیدهای خارجی تعریف شده ابتدا باید گروهها را اضافه کرد
Category ca = new Category() { CategoryName = "Sport" };
session.Save(ca);
Category ca2 = new Category() { CategoryName = "IT" };
session.Save(ca2);
Category ca3 = new Category() { CategoryName = "Business" };
session.Save(ca3);
//سپس یک کاربر را به دیتابیس اضافه میکنیم
User u = new User() { Password = "123$5@1", UserName = "VahidNasiri" };
session.Save(u);
//اکنون میتوان یک خبر جدید را ثبت کرد
News news = new News()
{
Category = ca,
User = u,
DateEntered = DateTime.Now,
Id = Guid.NewGuid(),
NewsText = "متن خبر جدید",
Subject = "عنوانی دلخواه"
};
session.Save(news);
transaction.Commit(); //پایان تراکنش
}
}
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
و یا میتوان از LINQ استفاده کرد:
برای مثال کاربر VahidNasiri تعریف شده را یافته، اطلاعات آنرا نمایش دهید؛ سپس نام او را به Vahid ویرایش کرده و دیتابیس را به روز کنید.
برای اینکه کوئریهای LINQ ما شبیه به LINQ to SQL شوند، کلاس NewsContext را به صورت ذیل تشکیل میدهیم. این کلاس از کلاس پایه NHibernateContext مشتق شده و سپس به ازای تمام موجودیتهای برنامه، یک متد از نوع IOrderedQueryable را تشکیل خواهیم داد.
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample2.Domain;
namespace NHSample2
{
class NewsContext : NHibernateContext
{
public NewsContext(ISession session)
: base(session)
{ }
public IOrderedQueryable<News> News
{
get { return Session.Linq<News>(); }
}
public IOrderedQueryable<Category> Categories
{
get { return Session.Linq<Category>(); }
}
public IOrderedQueryable<User> Users
{
get { return Session.Linq<User>(); }
}
}
}
using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using System.Linq;
using NHSample2.Domain;
namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
using (NewsContext db = new NewsContext(session))
{
var query = from x in db.Users
where x.UserName == "VahidNasiri"
select x;
//اگر چیزی یافت شد
if (query.Any())
{
User vahid = query.First();
//نمایش اطلاعات کاربر
Console.WriteLine("Id: {0}, UserName: {0}", vahid.Id, vahid.UserName);
//به روز رسانی نام کاربر
vahid.UserName = "Vahid";
session.Update(vahid);
transaction.Commit(); //پایان تراکنش
}
}
}
}
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
اگر به اسکریپت دیتابیس تولید شده دقت کرده باشید، عملیات AutoMapping یک سری پیش فرضهایی را اعمال کرده است. برای مثال فیلد Id را از نوع identity و به صورت کلید تعریف کرده، یا رشتهها را به صورت nvarchar با طول 255 ایجاد نموده است. امکان سفارشی سازی این موارد نیز وجود دارد.
مثال:
using FluentNHibernate.Conventions.Helpers;
public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());
new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add(
PrimaryKey.Name.Is(x => "ID"),
DefaultLazy.Always(),
ForeignKey.EndsWith("ID"),
Table.Is(t => "tbl" + t.EntityType.Name)
)
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);
return cfg;
}
تابع GenerateMapping معرفی شده را اینجا با قسمت Conventions.Add تکمیل کردهایم. به این صورت دقیقا مشخص شده است که فیلدهایی با نام ID باید primary key در نظر گرفته شوند، همواره lazy loading صورت گیرد و نام کلید خارجی به ID ختم شود. همچنین نام جداول با tbl شروع گردد.
روش دیگری نیز برای معرفی این قرار دادها و پیش فرضها وجود دارد. فرض کنید میخواهیم طول رشته پیش فرض را از 255 به 500 تغییر دهیم. برای اینکار باید اینترفیس IPropertyConvention را پیاده سازی کرد:
using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Instances;
namespace NHSample2.Conventions
{
class MyStringLengthConvention : IPropertyConvention
{
public void Apply(IPropertyInstance instance)
{
instance.Length(500);
}
}
}
public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());
new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add<MyStringLengthConvention>()
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);
return cfg;
}
نکته:
اگر برای یافتن اطلاعات بیشتر در این مورد در وب جستجو کنید، اکثر مثالهایی را که مشاهده خواهید کرد بر اساس نگارش بتای fluent NHibernate هستند و هیچکدام با نگارش نهایی این فریم ورک کار نمیکنند. در نگارش رسمی نهایی ارائه شده، تغییرات بسیاری صورت گرفته که آنها را در این آدرس میتوان مشاهده کرد.
دریافت سورس برنامه قسمت ششم
ادامه دارد ...
ماژولها در ES 6
هدف از سیستم ماژولها در ES 6، مدیریت بهتر تعدادی قطعه کد جاوا اسکریپتی، به صورت یک واحد مشخص است. همچنین ماژولها امکان مخفی کردن قسمتهایی از کد را که نباید به صورت عمومی در دسترس قرارگیرند، نیز میسر میکنند. این مسایل سالها آرزوی برنامه نویسان جاوا اسکریپت بودهاند و برای برآورده کردن آنها به روشهای غیراستاندارد و کتابخانههای ثالثی روی آورده بودند. به همین جهت برای آشنایی بهتر با ماژولها در ES 6، ابتدا نیاز است با روشهای متداول فعلی بسته بندی کدها در جاوا اسکریپت آشنا شد.
روش IIFE Module
الگوی ماژولها، سالها است که در جاوا اسکریپت مورد استفادهاست:
(function(target){ var privateDoWork = function(name) { return name +" is working"; }; var Employee = function(name) { this.name = name; } Employee.prototype = { doWork: function() { return privateDoWork(this.name); } } target.Employee = Employee; }(window));
بنابراین با روش IIFE به مزیتهای یک سیستم ماژول میرسیم:
الف) امکان مدیریت کدها را به صورت یک unit و واحد فراهم میکند.
ب) همچنین در اینجا امکان کنترل میدان دید متغیرها و متدها نیز میسر است.
روش CommonJS
از سال 2009 استفاده از جاوا اسکریپت به خارج از مرورگرها گسترش یافت؛ برای مثال نوشتن برنامههای سمت سرور NodeJS یا MongoDB با جاوا اسکریپت. در یک چنین حالتی برای مدیریت پیچیدگی برنامههای گستردهی سمت سرور و پرهیز از متغیرها و اشیاء عمومی، پروژهی CommonJS شکل گرفت. در CommonJS نحوهی تعریف ماژولها بسیار شبیه است به IIFE. با این تفاوت که دیگر خبری از متد خود اجرا شونده وجود ندارد و همچنین بجای target از exports، جهت درمعرض دید قرار دادن اشیاء استفاده میکند.
var privateDoWork = function(name) { return name +" is working"; }; var Employee = function(name) { this.name = name; } Employee.prototype = { doWork: function() { return privateDoWork(this.name); } } exports.Employee = Employee;
var Employee = require("./Employee").Employee; var e1 = new Employee("Vahid"); console.log(e1.doWork());
روش AMD
از CommonJS بیشتر در برنامههای جاوا اسکریپتی که خارج از مرورگر اجرا میشوند، استفاده میشود. برای حالتهای اجرای برنامهها درون مرورگرها و خصوصا بلاک نشدن ترد نمایش صفحه در حین پردازش ماژولها، روش دیگری به نام AMD API و یا Asynchronous module definition به وجود آمد. پیاده سازی محبوب این API عمومی، توسط کتابخانهای به نام RequireJS انجام شدهاست.
define(function(){ var privateDoWork = function(name) { // ... }; var Employee = function(name) { // ... } return Employee; });
تفاوت مهم این روش با روش IIFE این است که در روش IIFE تمام کد باید مهیا بوده و همچنین بلافاصله قابل اجرا باشد. در اینجا تنها زمانیکه نیاز به کار با ماژولی باشد، اطلاعات آن بارگذاری شده و استفاده میشود.
برای استفادهی از این ماژولها نیز از همان define استفاده میشود و پارامتر اول ارسالی، آرایهای است که ماژولهای مورد نیاز را تعریف میکند (تعریف وابستگیها). برای مثال employee تعریف شده در اینجا سبب بارگذاری فایل employee.js میشود و سپس امکانات آن به صورت یک پارامتر، به متدی که به آن نیاز دارد ارسال میگردد:
define(["employee"], function(Employee){ var e = new Employee("Vahid"); });
ماژولها در ES 6
سیستم تعریف ماژولها در ES 6 بسیار شبیه است به روشهای CommonJS و AMD API. در اینجا یک نمونه از روش تعریف ماژولها را در نگارش جدید جاوا اسکریپت مشاهده میکنید:
export class Employee { constructor(name) { this[s_name] = name; } get name() { return this[s_name]; } doWork() { return `${this.name} is working`; } }
پس از این export، اکنون برای استفادهی از آن در یک فایل js دیگر، از واژهی کلیدی import کمک گرفته میشود:
import {Employee} from "./employee"; var e1 = new Employee("Vahid"); console.log(e1.doWork());
و یا برای ارائهی یک متد خروجی، به نحو ذیل عمل میشود:
export function multiply (x, y) { return x * y; };
var hello = 'Hello World', multiply = function (x, y) { return x * y; }; export { hello, multiply };
حالت پیش فرض ماژولهای ES 6 همان strict mode است
در سیستم ماژولهای ES 6، حالت strict به صورت پیش فرض روشن است. برای مثال متغیرها حتما باید تعریف شوند.
امکان تعریف خروجیهای متفاوت از یک ماژول در ES 6
در همان فایلی که export class Employee فوق را در آن تعریف کردهایم، یک چنین تعریفهایی را نیز میتوان ارائه داد:
export let log = function(employee) { console.log(employee.name); } export let defaultRaise = 0.03; export let modelEmployee = new Employee("Vahid");
import {Employee, log, defaultRaise, modelEmployee} from "./employee"; log(modelEmployee);
module m from "./employee";
console.log(m.defaultRaise);
var e1 = new m.Employee("Vahid"); console.log(e1.doWork());
روش دیگر انجام اینکار، استفاده از * است برای درخواست تمام وابستگیهای مورد نیاز:
import * from "./employee";
امکان استفاده از یک ماژول در ماژولی دیگر
برای اینکه از امکانات یک ماژول در ماژولی دیگر استفاده کنیم نیز میتوان از همان روش تعریف import در ابتدای ماژول استفاده کرد:
import {Employee} from "./employee";
امکان تعریف ماژول پیش فرض در ES 6
اگر ماژول شما (همان فایل js) تنها دارای یک export است، میتوانید آنرا با واژهی کلیدی default مشخص کنید:
export default class Employee {
import factory from "./employee"; var e1 = new factory("Vahid"); console.log(e1.doWork());
البته باید دقت داشت که یک چنین تعریفهایی نیز مجاز است و میتوان خروجی پیش فرض و همچنین نامداری را نیز با هم ترکیب کرد:
export hello = 'Hello World'; export default function (x, y) { return x * y; };
import pow2, { hello } from 'modules';
امکان مخفی سازی اطلاعات در ماژولهای ES 6
یکی از انتظارات از سیستم ماژول، امکان مخفی سازی اطلاعات است. در اینجا تنها کافی است شیء، متد و یا متغیر تعریف شده، با واژهی کلیدی export مزین نشوند:
let privateFunction = function() { } export default class Employee {
یک نکته: اگر در کلاس export شده، خواستید تا دسترسی به s_name را محدود کنید، از Symbol ها به نحو ذیل کمک بگیرید:
let s_name = Symbol(); export class Employee { constructor(name) { this[s_name] = name; } get name() { return this[s_name]; } doWork() { return `${this.name} is working`; } }
-
تنها دریافت رکوردهای مورد نیاز
string city = "New York"; List<School> schools = db.Schools.ToList(); List<School> newYorkSchools = schools.Where(s => s.City == city).ToList();
در کد بالا ابتدا کلیه ردیفهای جدول از دیتابیس به حافظه منتقل میشود و سپس برروی آنها کوئری مورد نظر اعمال میگردد که بشدت میتواند برای یک برنامه - خصوصا برنامه وب - بهدلیل دریافت کلیهی ردیفهای جدول بسیار مخرب باشد. کوئری فوق را میتوان به صورت زیر اصلاح کرد:
List<School> newYorkSchools = db.Schools.Where(s => s.City == city).ToList(); یا IQueryable<School> schools = db.Schools; List<School> newYorkSchools = schools.Where(s => s.City == city).ToList();
-
حداقل رفت و برگشت به دیتابیس
کد زیر را در نظر بگیرید:
string city = "New York"; List<School> schools = db.Schools.Where(s => s.City == city).ToList(); var sb = new StringBuilder(); foreach(var school in schools) { sb.Append(school.Name); sb.Append(": "); sb.Append(school.Pupils.Count); sb.Append(Environment.NewLine); }
هدف تکه کد بالا این است که تعداد دانش آموزان مدرسههای واقع در شهر New York را بدست آورد.
توجه داشته باشید:
- یک مدرسه میتواند چندین دانش آموز داشته باشد (وجود رابطه یک به چند)
- LazyLoading فعال است
- تعداد مدرسههای شهر نیویورک 200 عدد میباشد
اگر کوئری بالا را بهوسیلهی یک پروفایلر بررسی نمایید، متوجه خواهید شد 1 + 200 رفت و برگشت به دیتابیس صورت گرفته است که به "N+1 select problem" معروف است. 1 مرتبه جهت دریافت لیست مدرسههای شهر نیویورک و 200 مرتبه جهت دریافت تعداد دانش آموزان هر مدرسه.
بدلیل فعال بودن Lazy Loading، زمانیکه موجودیتی فراخوانی میشود، سایر موجودیتهای وابسته به آن، زمانی از دیتابیس فراخوانی خواهند شد که به آنها دسترسی پیدا کنید. در حلقهی foreach هم به ازای هر مدرسه (200 مدرسه) شهر نیویورک یک رفت و برگشت انجام میشود.
اما راه حل در این مورد خاص استفاده از Eager Loading است. خط دوم کد را بصورت زیر تغییر دهید:
List<School> schools = db.Schools .Where(s => s.City == city) .Include(x => x.Pupils) .ToList();
حال با یک رفت و برگشت، همراه هر مدرسه اطلاعات مربوط به دانش آموزان وابستهی آن نیز در دسترس خواهد بود.
-
تنها استفاده از ستونهای مورد نیاز
فرض کنید قصد دارید نام و نام خانوادگی دانش آموزان یک مدرسه را بدست آورید.
int schoolId = 1; List<Pupil> pupils = db.Pupils .Where(p => p.SchoolId == schoolId) .ToList(); foreach(var pupil in pupils) { textBox_Output.Text += pupil.FirstName + " " + pupil.LastName; textBox_Output.Text += Environment.NewLine; }
- انتقال اطلاعات بلا استفاده که ممکن است باعث کاهش کارآیی Sql Server I/O و شبکه و اشغال حافظهی کلاینت گردد.
- کاهش کارآیی ایندکس گذاری. فرض کنید برروی جدول دانش آموزان ایندکسی شامل 2 ستون نام و نام خانوادگی تعریف کردهاید. با انتخاب تمام ستونهای جدول توسط خط دوم (select * from...) به کارآیی ایندکس گذاری برروی این جدول آسیب زدهاید. توضیح بیشتر در اینجا مطرح شده است.
اما راه حل:
var pupils = db.Pupils .Where(p => p.SchoolId == schoolId) .Select(x => new { x.FirstName, x.LastName }) .ToList();
-
عدم تطابق نوع ستون با نوع خصیصه مدل
فرض کنید نوع ستون جدول دانش آموزان (VARCHAR(20 است و خصیصه کدپستی مدل دانش آموز مانند زیر تعریف شده است:
public string PostalZipCode { get; set; }
انتخاب نوع داده و تطابق نوع داده مدل با ستون جدول دارای اهمیت زیادی است و در صورت عدم رعایت، باعث کاهش کارآیی شدید میگردد. در کد زیر قصد دارید لیست نام و نام خانوادگی دانش آموزانی را که کدپستی آنها 90210 میباشد، بدست بیاورید.
string zipCode = "90210"; var pupils = db.Pupils .Where(p => p.PostalZipCode == zipCode) .Select(x => new {x.FirstName, x.LastName}) .ToList();
هنگامیکه کوئری بالا را اجرا نمایید، زمان زیادی جهت اجرای آن صرف خواهد شد. در صورتی که از یک پروفایلر استفاده نمایید، میتوانید عملیات پرهزینه را شناسایی نمایید و اقدام به کاهش هزینهها کنید.
همانطور که در شکل بالا مشخص است عملیات index scan از سایر عملیاتها پرهزینهتر است. حال به بررسی علت بهوجود آمدن این عملیات پرهزینه خواهیم پرداخت.
Index Scan زمانی رخ میدهد که اس کیو ال سرور مجبور است هر صفحهی از ایندکس را بخواند و شرایط را (کدپستی برابر 90210) اعمال نماید و نتیجه را برگرداند. Index Scan بسیار هزینه بر است، چون اس کیو ال سرور، کل ایندکس را بررسی مینماید. نقطهی مقابل و بهینهی آن، Index Seek است که اس کیو ال سرور به صفحهی مورد نظر ایندکسی که به شرایط نزدیکتر است، منتقل میگردد.
خب چرا اس کیو ال سرور Index Scan را بجای Index Seek انتخاب کرده است؟!
اشکالی در قسمت سمت چپ شکل بالا که به رنگ قرمز نمایش داده شده است، وجود دارد:
Type conversion: Seek Plan for CONVERT_IMPLICIT(nvarchar(20), [Extent1].[PostalZipCode],0)=[@p__linq__0]
پارامتر کوئری تولید شدهی توسط EF از نوع NVARCHAR است و تبدیل نوع NVARCHAR پارامتر کدپستی، که محدودهی اطلاعات بیشتری (Unicode Strings) را نسبت به نوع VARCHAR ستون دارد، بهدلیل از دست رفتن اطلاعات امکان پذیر نیست. بههمین جهت برای مقایسهی پارامتر کدپستی با ستون VARCHAR ، اس کیو ال سرور باید هر ردیف ایندکس را از VARCHAR به NVARCHAR تبدیل نماید که منجر به Index Scan میشود. اما راه حل بسیار ساده این است که فقط نوع خصیصه را با ستون جدول یکسان کنید.
[Column(TypeName = "varchar")] public string PostalZipCode { get; set; }
ListBox
در مورد لیست، ما قبلا نام کشورها را با استفاده از تگ ListBoxItem به طور دستی اضافه میکردیم و هر گونه ویرایش و اضافه کردن عکس و دیگر اشیاء را داخل این تگ برای هر آیتم جداگانه انجام میدادیم؛ مثل تصویر زیر که هر آیتم شامل یک تگ تصویر و دو تگ TextBlock است که یکی از آنها رنگی شده است. کد هر آیتم به طور جداگانه و دستی اضافه شده است.
<ListBox Grid.Row="3" Name="MyListBox" Grid.Column="1" Margin="10" Height="80" > <ListBox.ItemTemplate> <DataTemplate> <WrapPanel> <Image Width="24" Height="24" Source="{Binding Flag}"></Image> <TextBlock Padding="5 5 0 0" Text="{Binding Name}"></TextBlock> </WrapPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
ارائه این نکته ضروری است که همه اشیاء خصوصیت DataContext را دارند و ما در مثال قبلی DataContext ریشه یا والد اشیاء را پر کردیم. اگر مقاله "ساختار سلسله مراتبی " را به یاد بیاورید، گفتیم که هر شیء در صورتیکه خصوصیت وابستهای برایش تعریف نشده باشد، به سمت اشیاء والد حرکت میکند، به این جهت بود که همهی کنترلها به منبع دادهها دسترسی داشتند. پس ما اگر DataContext لیست را پر کنیم، لیست دلیلی برای دسترسی به DataContext اشیاء والد ندارد و خصوصیت پر شدهی خودش را در نظر میگیرد. پس بیایید این مورد را امتحان کنیم:
من کلاس زیر را جهت ارسال لیستی از کشورها به همراه آدرس پرچمشان، بر میگردانم:
دلیل استفاده از کلاس ObservableCollection در کد زیر به جای استفاده از اشیایی چون Ilist و ... این بود که این کلاس به اینترفیس هایی چون INotifyPropertyChanged مزین گشته و هر گونه تغییری در این مجموعه، از قبیل حذف و اضافه را اطلاع رسانی کرده و مدل تغییر یافته را به سمت ویو هدایت میکند.
using System.Collections.ObjectModel; namespace test { public class Country { public string Flag { get { return "Images/flags/" + Name + ".png"; } } public string Name { get; set; } public int Id { get; set; } public ObservableCollection<Country> GetCountries() { var countries = new ObservableCollection<Country>(); countries.Add(new Country(){Id =1,Name = "Afghanistan"}); countries.Add(new Country() { Id = 2, Name = "Albania" }); countries.Add(new Country() { Id = 3, Name = "Angola" }); countries.Add(new Country() { Id = 4, Name = "Bahrain" }); countries.Add(new Country() { Id = 5, Name = "Bermuda" }); countries.Add(new Country() { Id =6, Name = "Iran" }); return countries; } } }
دلیل این مشکل این است که DataContext برای نمایش یک Object تهیه شده است و در مورد دادههای لیستی باید از خصوصیتی به نام ItemsSource استفاده کرد که برای دادههای لیستی IEnumerables، بهینه شده است.
پس به این ترتیب مینویسیم :
public MainWindow() { InitializeComponent(); person = Person.GetPerson(); DataContext = person; //خط جدید MyListBox.ItemsSource = new Country().GetCountries(); }
شکلهای زیر یک نمودار از ارتباط با Object برای واکشی داده هاست:
نمودار زیر هم دسترسی به مجموعه ای از دادههای لیستی است که از طریق ItemsSource خوانده میشوند:
کد زیر همچنین برای اتصال به کار میرود:
public MainWindow() { InitializeComponent(); person = Person.GetPerson(); DataContext = person; //خط جدید MyListBox.DataContext = new Country().GetCountries(); MyListBox.SetBinding(ItemsControl.ItemsSourceProperty, new Binding()); }
پی نوشت : روشهای دیگر بایند کردن همچون استفاده از منابع یا ریسورسها یا استفاده از ViewModelها هم هستند که در آینده در مورد آنها بیشتر صحبت خواهیم کرد.
حال که توانستیم لیست را پر کنیم باید کشوری را که در رکورد واکشی شده آمده است، در لیست انتخاب کنیم.
توجه داشته باشید که باید لیست را از طریق خصوصیت ItemsSource پر کرده باشید و DataContext را دستکاری نکرده باشید.
خصوصیت Country در کلاس Person میتواند به دو صورت زیر باشد:
public int Country { get; set; } public Country Country { get; set; }
<ListBox Grid.Row="3" Name="MyListBox" Grid.Column="1" Margin="10" Height="80" SelectedValuePath="Id" SelectedValue="{Binding Country}" > <ListBox Grid.Row="3" Name="MyListBox" Grid.Column="1" Margin="10" Height="80" SelectedValuePath="Id" SelectedValue="{Binding Country.Id}" >
خصوصیتهای دیگر یک شیء لیستی چون ListBox و ComboBox و ... SelectedIndex است که اندیس یک آیتم انتخابی را بازگردانده یا جهت انتخاب یک آیتم، اندیس آن را دریافت میکند. SelectedItem و SelectedItems هم شیء یا شیءهایی از مدل را (در اینجا Country) که در لیست انتخاب شدهاند، بر میگرداند (فقط خواندنی).
DateTime thisDate1 = new DateTime(2011, 6, 10); Console.WriteLine("Today is " + thisDate1.ToString("MMMM dd, yyyy") + ".");
بازگرداندن Stream فایل از WCF
به نظرم در مثال سیلورلایت فایل PDF روی سرور ذخیره میشود و بعد به کاربر نمایش داده میشود
آیا لازم است که فایل روی سرور ذخیره شود یعنی آیا میتوان فایل را به صورت Stream تولید کرد
.Generate(data => data.AsPdfStream(new MemoryStream()));
و بعد PdfStreamOutput آن را بازگرداند؟
دریافت و نصب AvalonEdit
برای نصب AvalonEdit میتوان دستور ذیل را در کنسول پاورشل نیوگت صادر کرد:
PM> install-package AvalonEdit
استفادهی مقدماتی از AvalonEdit
برای استفاده از این ویرایشگر ابتدا نیاز است فضای نام xmlns:avalonEdit تعریف شود. سپس کنترل avalonEdit:TextEditor در دسترس خواهد بود:
<Window x:Class="SyntaxHighlighter.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" Title="MainWindow" Height="401" Width="617"> <Grid> <avalonEdit:TextEditor Name="txtCode" SyntaxHighlighting="C#" FontFamily="Consolas" FontSize="10pt"/> </Grid> </Window>
استفاده از AvalonEdit در برنامههای MVVM
خاصیت Text این ویرایشگر به صورت معمولی تعریف شده (DependencyProperty نیست) و امکان binding دو طرفه به آن وجود ندارد. به همین جهت نیاز است یک چنین DependencyProperty را به آن اضافه کرد:
using System; using System.Collections.Concurrent; using System.Reflection; using System.Windows; using System.Xml; using ICSharpCode.AvalonEdit; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Highlighting.Xshd; namespace AvalonEditWpfTest.Controls { public class BindableAvalonTextEditor : TextEditor { public static readonly DependencyProperty BoundTextProperty = DependencyProperty.Register("BoundText", typeof(string), typeof(BindableAvalonTextEditor), new FrameworkPropertyMetadata(default(string), propertyChangedCallback)); public static string GetBoundText(DependencyObject obj) { return (string)obj.GetValue(BoundTextProperty); } public static void SetBoundText(DependencyObject obj, string value) { obj.SetValue(BoundTextProperty, value); } protected override void OnTextChanged(EventArgs e) { SetCurrentValue(BoundTextProperty, Text); base.OnTextChanged(e); } private static void propertyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args) { var target = (BindableAvalonTextEditor)obj; var value = args.NewValue; if (value == null) return; if (string.IsNullOrWhiteSpace(target.Text) || !target.Text.Equals(args.NewValue.ToString())) { target.Text = args.NewValue.ToString(); } } } }
اکنون برای استفاده از این کنترل جدید که BindableAvalonTextEditor نام دارد، میتوان به نحو ذیل عمل کرد:
<Window x:Class="AvalonEditWpfTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:viewModels="clr-namespace:AvalonEditTests.ViewModels" xmlns:controls="clr-namespace:AvalonEditWpfTest.Controls" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <viewModels:MainWindowViewModel x:Key="MainWindowViewModel"/> </Window.Resources> <Grid DataContext="{Binding Source={StaticResource MainWindowViewModel}}"> <controls:BindableAvalonTextEditor BoundText="{Binding SourceCode, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" WordWrap="True" ShowLineNumbers="True" LineNumbersForeground="MediumSlateBlue" FontFamily="Consolas" VerticalScrollBarVisibility="Auto" Margin="3" HorizontalScrollBarVisibility="Auto" FontSize="10pt"/> </Grid> </Window>
افزودن syntax highlighting زبانهایی که به صورت رسمی پشتیبانی نمیشوند
به خاصیت SyntaxHighlighting این کنترل صرفا مقادیری را میتوان نسبت داد که به صورت توکار پشتیبانی میشوند. برای مثال#XML، C و امثال آن.
فرض کنید نیاز است SyntaxHighlighting زبان SQL را فعال کنیم. برای اینکار نیاز به فایلهای ویژهای است، با پسوند xshd. برای نمونه فایل sql-ce.xshd را در اینجا میتوانید مطالعه کنید. در آن یک سری واژههای کلیدی و حروفی که باید با رنگی متفاوت نمایش داده شوند، مشخص میگردند.
برای استفاده از فایل sql-ce.xshd باید به نحو ذیل عمل کرد:
الف) فایل sql-ce.xshd را به پروژه اضافه کرده و سپس در برگهی خواص آن در VS.NET، مقدار build action آنرا به embedded resource تغییر دهید.
ب) با استفاده از متد ذیل، این فایل مدفون شده در اسمبلی را گشوده و به متد HighlightingLoader.Load ارسال میکنیم:
private static IHighlightingDefinition getHighlightingDefinition(string resourceName) { if (string.IsNullOrWhiteSpace(resourceName)) throw new NullReferenceException("Please specify SyntaxHighlightingResourceName."); using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) { if (stream == null) throw new NullReferenceException(string.Format("{0} resource is null.", resourceName)); using (var reader = new XmlTextReader(stream)) { return HighlightingLoader.Load(reader, HighlightingManager.Instance); } } }
txtCode.SyntaxHighlighting = getHighlightingDefinition(resourceName);
برای سهولت استفاده از این قابلیت شاید بهتر باشد یک DependencyProperty دیگر به نام SyntaxHighlightingResourceName را به کنترل جدید BindableAvalonTextEditor اضافه کنیم:
using System; using System.Collections.Concurrent; using System.Reflection; using System.Windows; using System.Xml; using ICSharpCode.AvalonEdit; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Highlighting.Xshd; namespace AvalonEditWpfTest.Controls { public class BindableAvalonTextEditor : TextEditor { public static readonly DependencyProperty SyntaxHighlightingResourceNameProperty = DependencyProperty.Register("SyntaxHighlightingResourceName", typeof(string), typeof(BindableAvalonTextEditor), new FrameworkPropertyMetadata(default(string), resourceNamePropertyChangedCallback)); public static string GetSyntaxHighlightingResourceName(DependencyObject obj) { return (string)obj.GetValue(SyntaxHighlightingResourceNameProperty); } public static void SetSyntaxHighlightingResourceName(DependencyObject obj, string value) { obj.SetValue(SyntaxHighlightingResourceNameProperty, value); } private static void loadHighlighter(TextEditor @this, string resourceName) { if (@this.SyntaxHighlighting != null) return; @this.SyntaxHighlighting = getHighlightingDefinition(resourceName); } private static void resourceNamePropertyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args) { var target = (BindableAvalonTextEditor)obj; var value = args.NewValue; if (value == null) return; loadHighlighter(target, value.ToString()); } } }
استفاده از آن نیز به شکل زیر است:
<controls:BindableAvalonTextEditor SyntaxHighlightingResourceName = "AvalonEditWpfTest.Controls.sql-ce.xshd" />
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
AvalonEditWpfTest.zip
مثالی از using declarations
تا پیش از C# 8.0، روش متداول کار با عبارات using به صورت زیر است و به آن استفاده از using statements گفته میشود:
class Program { static void UsingOld() { using (var file = new FileStream("input.txt", FileMode.Open)) using (var reader = new StreamReader(file)) { var s = reader.ReadToEnd(); // Do something with data } }
اکنون در C# 8.0 میتوان قطعه کد فوق را به کمک using declarations به صورت زیر خلاصه کرد:
class Program { static void UsingNew(string[] args) { using Stream file = new FileStream("input.txt", FileMode.Open); using StreamReader reader = new StreamReader(file); var s = reader.ReadToEnd(); // Do something with data }
میدان دید using declarations
پس از این تغییرات، سؤال مهمی که مطرح میشود این است: متغیرهایی که توسط using declaration تعریف میشوند، تا چه زمانی زنده نگه داشته میشوند. به عبارتی متد UsingOldScope آیا همانند متد UsingNewScope عمل میکند؟ آیا متغیر buffer آن همانند متد UsingOldScope خارج از میدان دید usingها قرار میگیرد؟
class Program { static void UsingNewScope() { string buffer = null; using Stream file = new FileStream("input.txt", FileMode.Open); using StreamReader reader = new StreamReader(file); buffer = reader.ReadToEnd(); // Do something with data buffer = null; } static void UsingOldScope(string[] args) { string buffer = null; using (var file = new FileStream("input.txt", FileMode.Open)) using (var reader = new StreamReader(file)) { buffer = reader.ReadToEnd(); } // Do something with data buffer = null; }
اما زمانیکه از using declarations استفاده میشود (مانند متد UsingNewScope)، دیگر این {} را نداریم. اینبار scope تعریف شده، تا «پایان متد» ادامه پیدا میکند و سپس متد Dispose اشیاء ارجاعی، فراخوانی میگردد. بدیهی است در اینجا نیز همانند قبل، همان قطعه کد try/finally توسط کامپایلر جهت فراخوانی متد Dispose، تشکیل خواهد شد. بنابراین اگر بخواهیم متد UsingNewScope را توسط using statements پیشین بازنویسی کنیم، به یک چنین قطعه کدی خواهیم رسید که scope پس از using declarations، تا آخر متد ادامه پیدا میکند:
string buffer = null; using (var file = new FileStream("input.txt", FileMode.Open)) { using (var reader = new StreamReader(file)) { buffer = reader.ReadToEnd(); buffer = null; } }
پاسخ: بله. میتوان با تعریف یک {}، میدان دید متغیرهای ارجاعی توسط using declarations را محدود کرد:
private static void UsingDeclarationWithScope() { { using var r1 = new AResource(); r1.UseIt(); } // r1 is disposed here! Console.WriteLine("r1 is already disposed"); }
سؤال: آیا using declarations تمام قابلیتهای using statements را ارائه میدهند؟
پاسخ: خیر. فرض کنید کلاس AResource از نوع IDisposable تعریف شدهاست:
public class AResource : IDisposable { public void UseIt() => Console.WriteLine(nameof(UseIt)); public void Dispose() => Console.WriteLine($"Dispose {nameof(AResource)}"); }
class Program { public static AResource GetTheResource() => new AResource();
using (GetTheResource()) { // do something here } // resource is disposed here
using GetTheResource(); // Compiler error
using var _ = GetTheResource(); // Works fine