in robots.txt file: Sitemap: http://www.site.com/Sitemap
آشنایی با Leaflet
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script> <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
#map { height: 600px; }
var map = L.map('map').setView([29.6760859,52.4950737], 13);
var osmUrl='http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; var osmAttrib='Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors'; var osm = new L.TileLayer(osmUrl, { maxZoom: 18, attribution: osmAttrib}).addTo(map);
Marker، دایره و چندضلعی
در کنار نمایش Tileها میتوان اشکال گرافیکی نیز به نقشه اضافه کرد؛ مثل مارکر(نقطه)، مستطیل، دایره و یا یک Popup. اضافه کردن یک Marker به سادگی، با کد زیر صورت میپذیرد:
var marker = L.marker([29.623116,52.497856]).addTo(map);
محل مورد نظر به شیء مارکر پاس داده شده و مقدار بازگشتی به map اضافه شده است.
نمایش چند ضلعی و دایره هم کار ساده ای است. برای دایره باید ابتدا مختصات مرکز دایره و شعاع به متر را به L.circle پاس داد:
var circle = L.circle([29.6308217,52.5048021], 500, { color: 'red', fillColor: '#f03', fillOpacity: 0.5 }).addTo(map);
در کد بالا علاوه بر محل و اندازه دایره، رنگ محیط، رنگ داخل و شفافیت (opacity) نیز مشخص شدهاند.
برای چند ضلعیها میتوان به این صورت عمل کرد:
var polygon = L.polygon([ [29.628453, 52.488838], [29.637368, 52.493987], [29.637168, 52.503987] ]).addTo(map);
کار کردن با Popup ها
از Popup میتوان برای نمایش اطلاعات اضافهای بر روی یک محل خاص یا یک عنوان به مانند Marker استفاده کرد. برای مثال میتوان اطلاعاتی دربارهی محل یک Marker یا دایره نمایش داد. در هنگام ایجاد marker، دایره و چندضلعی مقادیر بازگشتی در متغیرهای جدایی ذخیره شدند. اکنون میتوان به آن اشیاء یک popup اضافه کرد:
marker.bindPopup("باغ عفیف آباد").openPopup(); circle.bindPopup("I am a circle."); polygon.bindPopup("I am a polygon.");
به علت اینکه openPopup برای Marker صدا زده شده، به صورت پیشفرض popup را نمایش میدهد. اما برای بقیه، نمایش با کلیک خواهد بود. البته الزاما نیازی نیست که popup روی یک شیء نمایش داده شود، میتوان popupهای مستقلی نیز ایجاد کرد:
var popup = L.popup() .setLatLng([51.5, -0.09]) .setContent("I am a standalone popup.") .openOn(map);
مشکل عمل نکردن فونت فارسی
public static class ReportMethod { static FontSelector fontSelector = new FontSelector(); const char RightToLeftEmbedding = (char)PersianDate.RightToLeftEmbedding; const char PopDirectionalFormatting = (char)PersianDate.PopDirectionalFormatting; public static Dictionary<string, string> fontDicBody = new Dictionary<string, string> { { "BMitra", "B Mitra" }, { "tahoma", "tahoma" } }; public static Dictionary<string, string> fontDicHeader1 = new Dictionary<string, string> { { "BMitraBd", "B Mitra Bold" }, { "tahoma", "tahoma" } }; public static Dictionary<string, string> fontDicHeader2 = new Dictionary<string, string> { { "BTitrBd", "B Titr Bold" }, { "tahoma", "tahoma" } }; public static Dictionary<string, string> fontDicFooter = new Dictionary<string, string> { { "BMitra", "B Mitra" }, { "tahoma", "tahoma" } }; public static string FixWeakCharacters(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; var weakCharacters = new[] { @"\", "/", "+", "-", "=", ";", "$" }; foreach (var weakCharacter in weakCharacters) { data = data.Replace(weakCharacter, RightToLeftEmbedding + weakCharacter + PopDirectionalFormatting); } return data; } public static Phrase SetFont(string data, int fontType) { Dictionary<string, string> fontDic; float fontSize = 11; switch (fontType) { case 0: fontDic = fontDicBody; fontSize = 11; break; case 1: fontDic = fontDicHeader1; fontSize = 14; break; case 2: fontDic = fontDicFooter; fontSize = 12; break; case 11: fontDic = fontDicHeader2; fontSize = 18; break; default: fontDic = fontDicBody; fontSize = 11; break; } foreach (var item in fontDic) { FontFactory.Register("c:\\windows\\fonts\\" + item.Key + ".ttf"); Font newfont = FontFactory.GetFont(item.Value, BaseFont.IDENTITY_H, fontSize); if (newfont.Familyname != "unknown") fontSelector.AddFont(newfont); } return fontSelector.Process(FixWeakCharacters(data)); } public static PdfPCell SetCell(string text, int border, int colspan, int Horizontal, int Vertical, bool DirectionRTL, int fontType = 0) { if (DirectionRTL) { var cell = new PdfPCell { RunDirection = PdfWriter.RUN_DIRECTION_RTL }; cell.Border = border; cell.Colspan = colspan; cell.HorizontalAlignment = Horizontal; cell.VerticalAlignment = Vertical; cell.Phrase = new Phrase(ReportMethod.SetFont(text, fontType)); return cell; } else { var cell = new PdfPCell(); cell.Border = border; cell.Colspan = colspan; cell.HorizontalAlignment = Horizontal; cell.VerticalAlignment = Vertical; cell.Phrase = new Phrase(ReportMethod.SetFont(text, fontType)); return cell; } } }
public IPdfReportData CreatePdfReport_SRptTeach(int MemberID, List<sp_Teach_Communicate_Select_ReportTeachResult> Teach_Result, string st, List<sp_Institute_Center_Info_Select_Name_MasterResult> Info) { string fileName = string.Format("SRptTeach-{0}.pdf", Guid.NewGuid().ToString("N")); return new PdfReport() .DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.RightToLeft); doc.Orientation(PageOrientation.Landscape); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { Author = Info[0].InstName, Application = "PdfRpt", Keywords = "گزارش", Subject = "گزارش ویژه", Title = "گزارش کارکرد مدرسید" }); doc.Compression(new CompressionSettings { EnableCompression = true, EnableFullCompression = true }); doc.PrintingPreferences(new PrintingPreferences { ShowPrintDialogAutomatically = false }); }) .DefaultFonts(fonts => { fonts.Path(System.IO.Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"), "fonts\\" + ReportMethod.fontDicBody.ElementAt(0).Key + ".ttf"), System.IO.Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"), "fonts\\" + ReportMethod.fontDicBody.ElementAt(1).Key + ".ttf")); fonts.Size(11); fonts.Color(System.Drawing.Color.Black); }) .PagesFooter(footer => { footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.RightToLeft)); }) .PagesHeader(header => { header.CustomHeader(new CustomHeader_SRptTeach(MemberID, st, Info)); }) .MainTableTemplate(template => { //template.BasicTemplate(BasicTemplate.SimpleTemplate); template.CustomTemplate(new TransparentTemplate()); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); table.GroupsPreferences(new GroupsPreferences { GroupType = GroupType.IncludeGroupingColumns, RepeatHeaderRowPerGroup = true, ShowOneGroupPerPage = false, SpacingBeforeAllGroupsSummary = 5f, ShowGroupingPropertiesInAllRows = true }); }) .MainTableDataSource(dataSource => { dataSource.StronglyTypedList<sp_Teach_Communicate_Select_ReportTeachResult>(Teach_Result); }) .MainTableSummarySettings(summarySettings => { summarySettings.OverallSummarySettings("جمع مبالغ"); summarySettings.AllGroupsSummarySettings("جمع کل مبالغ"); }) .MainTableColumns(columns => { columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.RowNo); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(0); column.Width(4); column.HeaderCell("ردیف"); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.ParentName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(1); column.Width(5); column.HeaderCell("مرکز"); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.FullName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(12); column.HeaderCell("نام و نام خانوادگی"); column.Group(true, (val1, val2) => { return val1.ToString() == val2.ToString(); }); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.TermInfoName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(3); column.Width(8); column.HeaderCell("ترم"); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Contract_NO); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(4); column.Width(7); column.HeaderCell("شماره قرارداد"); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.LessonFullCode); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(5); column.Width(4); column.HeaderCell("کد درس"); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.LessonName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(6); column.Width(10); column.HeaderCell("نام درس"); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Start_Date_Lesson); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(7); column.Width(6); column.HeaderCell("تاریخ شروع"); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.End_Date_Lesson); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(8); column.Width(6); column.HeaderCell("تاریخ پایان"); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.TeachAmount); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(9); column.Width(6); column.HeaderCell("مبلغ حق التدریس(ریال)"); column.ColumnItemsTemplate(template => { template.TextBlock(); template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.DoTeacherTime); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(10); column.Width(3); column.HeaderCell("ساعت کارکرد"); column.ColumnItemsTemplate(template => { template.TextBlock(); template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); column.AggregateFunction(aggregateFunction => { aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum); aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); }); columns.AddColumn(column => { column.PropertyName("CanPay"); column.CalculatedField(true, list => { if (list == null) return string.Empty; var amount = list.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.TeachAmount); var doTime = list.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.DoTeacherTime); var result = float.Parse(amount) * float.Parse(doTime); return Convert.ToDecimal(result); }); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(11); column.Width(7); column.HeaderCell("قابل پرداخت(ریال)"); column.ColumnItemsTemplate(template => { template.TextBlock(); template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); column.AggregateFunction(aggregateFunction => { aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum); aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); }); columns.AddColumn(column => { column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Personal_Education); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(12); column.Width(6); column.HeaderCell("مدرک تحصیلی"); }); //columns.AddColumn(column => //{ // column.PropertyName("Descriptions"); // column.CalculatedField(true, // list => // { // if (list == null) // return string.Empty; // var Row = list.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.RowNoPerson); // return ""; // }); // column.CellsHorizontalAlignment(HorizontalAlignment.Center); // column.IsVisible(true); // column.Order(13); // column.Width(3); // column.HeaderCell("توضیحات"); //}); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "اطلاعاتی برای نمایش وجود ندارد."); events.DocumentClosing(docClose => { string[] msgField = { "مدیر گروه", Info.Where(sp => sp.ID == MemberID).FirstOrDefault().InstKindName, Info.Where(sp => sp.ID == 0).FirstOrDefault().InstKindName, "امور مالی", "معاون پشتیبانی" }; string[] dataField = { "", Info.Where(sp => sp.ID == MemberID).FirstOrDefault().MasterName, Info.Where(sp => sp.ID == 0).FirstOrDefault().MasterName, "", Info.Where(sp => sp.ID == 1).FirstOrDefault().MasterName }; var infoTable = new PdfGrid(msgField.Length) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, WidthPercentage = 100 }; foreach (var item in msgField) { infoTable.AddCell(ReportMethod.SetCell(item, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true)); } foreach (var item in dataField) { infoTable.AddCell(ReportMethod.SetCell(item, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true)); } docClose.PdfDoc.Add(infoTable); }); }) .Export(export => { export.ToExcel(); export.ToCsv(); export.ToXml(); export.ToString(); }) .Generate(data => { fileName = HttpUtility.UrlEncode(fileName, Encoding.UTF8); data.FlushInBrowser(fileName, FlushType.Inline); }); //.Generate(data => data.AsPdfFile(string.Format("{0}\\PlansPage\\RptIListSample-{1}.pdf", AppPath.ApplicationPath, Guid.NewGuid().ToString("N")))); }
namespace Academy.Control.Reports { public class CustomHeader_SRptTeach : IPageHeader { public IPdfFont PdfRptFont { set; get; } string st; List<sp_Institute_Center_Info_Select_Name_MasterResult> Info; int MemberID; public CustomHeader_SRptTeach(int MemberID, string st, List<sp_Institute_Center_Info_Select_Name_MasterResult> Info) { this.st = st; this.Info = Info; this.MemberID = MemberID; } public PdfGrid RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> rowdata, IList<SummaryCellData> summaryData) { // return null; var groupFullName = rowdata.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.FullName); var groupPersonalEducation = rowdata.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Personal_Education); var table = new PdfGrid(2) { WidthPercentage = 100 }; table.AddSimpleRow( (cellData, cellProperties) => { cellData.Value = "نام و نام خانوادگی:"; cellProperties.PdfFont = PdfRptFont; cellProperties.PdfFontStyle = DocumentFontStyle.Bold; cellProperties.HorizontalAlignment = HorizontalAlignment.Left; }, (cellData, cellProperties) => { cellData.Value = groupFullName; cellProperties.PdfFont = PdfRptFont; cellProperties.HorizontalAlignment = HorizontalAlignment.Left; }); table.AddSimpleRow( (cellData, cellProperties) => { cellData.Value = "مدرک تحصیلی :"; cellProperties.PdfFont = PdfRptFont; cellProperties.PdfFontStyle = DocumentFontStyle.Bold; cellProperties.HorizontalAlignment = HorizontalAlignment.Left; }, (cellData, cellProperties) => { cellData.Value = groupPersonalEducation; cellProperties.PdfFont = PdfRptFont; cellProperties.HorizontalAlignment = HorizontalAlignment.Left; }); return table.AddBorderToTable(borderColor: BaseColor.LIGHT_GRAY, spacingBefore: 10f); } public PdfGrid RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData) { var tableMain = new PdfGrid(1) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, WidthPercentage = 100 }; tableMain.DefaultCell.Border = PdfPCell.NO_BORDER; PdfGrid table = new PdfGrid(3); table.DefaultCell.Border = PdfPCell.NO_BORDER; table.AddCell(ReportMethod.SetCell("", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false)); table.AddCell(ReportMethod.SetCell("گزارش کارکرد مدرسین ", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true,11)); PdfPTable tbRight = new PdfPTable(1) { RunDirection = PdfWriter.RUN_DIRECTION_RTL }; tbRight.DefaultCell.Border = PdfPCell.NO_BORDER; Image _image = Image.GetInstance(System.IO.Path.Combine(AppPath.ApplicationPath, "Content\\Images\\p_jahad2.jpg")); var cellImg = new PdfPCell(_image, false) { Border = PdfPCell.NO_BORDER }; cellImg.HorizontalAlignment = PdfPCell.ALIGN_CENTER; tbRight.AddCell(cellImg); tbRight.AddCell(ReportMethod.SetCell(Info[0].InstName, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false,1)); tbRight.AddCell(ReportMethod.SetCell(Info.Where(sp => sp.ID == MemberID).FirstOrDefault().SecondName, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false,1)); table.AddCell(tbRight); PdfGrid tbLeft = new PdfGrid(2) { RunDirection = PdfWriter.RUN_DIRECTION_RTL }; tbLeft.DefaultCell.Border = PdfPCell.NO_BORDER; tbLeft.AddCell(ReportMethod.SetCell("تاریخ گزارش : " + System.DateTime.Now.ToPersianDateTime("/", false), PdfPCell.NO_BORDER, 2, PdfPCell.ALIGN_LEFT, PdfPCell.ALIGN_MIDDLE, false)); tbLeft.AddCell(ReportMethod.SetCell("از تاریخ " + st.Split(';')[0], PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_LEFT, PdfPCell.ALIGN_MIDDLE, false)); tbLeft.AddCell(ReportMethod.SetCell("تا تاریخ " + st.Split(';')[1], PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_LEFT, PdfPCell.ALIGN_MIDDLE, false)); table.AddCell(tbLeft); table.AddCell(ReportMethod.SetCell("", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false)); table.AddCell(ReportMethod.SetCell("", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false)); tableMain.AddCell(table); return tableMain; } } }
تولید خودکار کدها بر اساس OpenAPI Specification
فرض کنید در حال توسعهی برنامهی سمت کلاینت Angular و یا سمت سرور ASP.NET Core ای هستید که هر دوی اینها از یک Web API استفاده میکنند. همچنین فرض کنید که این Web API را نیز خودتان توسعه میدهید. بنابراین حداقل کدی که باید در اینجا به اشتراک گذاشته شود، کدهای کلاسهای DTO یا Data transfer objects هستند تا این کلاینتها بتوانند اطلاعات Web API را به نحو صحیحی Deserialize کنند و یا برعکس، بتوانند اطلاعات را با فرمت صحیحی به سمت Web API ارسال کنند.
برای مدیریت این مساله میتوان از دو روش استفاده کرد:
الف) استفاده از یک پروژهی اشتراکی
اگر کدهای مدنظر، سمت سرور باشند، میتوان یک پروژهی اشتراکی را برای این منظور ایجاد کرد و کدهای DTO را درون آن قرار داد و سپس ارجاعی به آن را در پروژههای مختلف، استفاده نمود. به این ترتیب تکرار کدها، کاهش یافته و همچنین تغییرات آن نیز به تمام پروژههای استفاده کننده به نحو یکسانی اعمال میشوند. در این حالت یک اسمبلی اشتراکی تولید شده و به صورت مستقلی توزیع میشود.
ب) استفاده از روش لینک کردن فایلها
در این روش پروژههای استفاده کننده از کلاسهای DTO، فایلهای آنرا به پروژهی خود لینک میکنند. در این حالت باز هم شاهد کاهش تکرار کدها و همچنین اعمال یک دست تغییرات خواهیم بود. اما در این روش دیگر یک اسمبلی اشتراکی وجود نداشته و کلاسهای DTO هم اکنون با اسمبلی پروژههای استفاده کننده، یکی و کامپایل شدهاند.
بدیهی است در هر دو روش، نیاز است بر روی کلاینت و API، کنترل کاملی وجود داشته باشد و بتوان به کدهای آنها دسترسی داشت. به علاوه فایلهای اشتراکی نیز باید بر اساس Target platform یکسانی تولید شده باشند. در این حالت دیگر نیازی به OpenAPI Specification برای تولید کدهای کلاینت دات نتی خود، نیست.
اما اگر کدهای API مدنظر در دسترس نباشند و یا بر اساس پلتفرم دیگری مانند node.js تولید شده باشد، کار یکپارچه سازی با آن دیگر با به اشتراک گذاری فایلهای آن میسر نیست. در این حالت اگر این API به همراه یک OpenAPI Specification باشد، میتوان از آن برای تولید خودکار کدهای کلاینتهای آن استفاده کرد.
معرفی تعدادی از ابزارهایی که قادرند بر اساس OpenAPI Specification، کد تولید کنند
برای تولید کد از روی OpenAPI Specification، گزینههای متعددی در دسترس هستند:
الف) Swagger CodeGen
این ابزار را که جزئی از مجموعه ابزارهای تولید شدهی برفراز OpenAPI است، میتوانید از آدرس swagger-codegen دریافت کنید. البته برای اجرای آن نیاز به Java Runtime است و یا نگارش آنلاین آن نیز در دسترس است: swagger.io
در ابزار آنلاین آن، در منوی generate بالای صفحه، گزینهی تولید کد برای #C نیز موجود است.
ب) AutoRest
محل دریافت: https://github.com/Azure/autorest
بر اساس node.js کار میکند و از طریق خط فرمان، قابل دسترسی است. همچنین این مورد ابزار تامین کنندهی گزینهی Add REST client در ویژوال استودیو نیز میباشد. اما در کل، امکان تنظیمات آنچنانی را به همراه ندارد.
ج) NSwagStudio
محل دریافت: https://github.com/RSuter/NSwag/wiki/NSwagStudio
همانطور که در مطلب «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت اول - معرفی» نیز عنوان شد، NSwag یکی دیگر از تولید کنندههای OpenAPI Specification مخصوص پروژههای دات نت است. NSwagStudio نیز جزئی از این مجموعه است که به کمک آن میتوان کدهای کلاینتها و DTOها را بر اساس OpenAPI Spec تولید کرد. همچنین امکان تنظیمات قابل توجهی را در مورد نحوهی تولید کدهای نهایی به همراه دارد.
استفاده از NSwagStudio برای تولید کدهای DTOها
در اینجا از همان برنامهای که در سری «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger» بررسی کردیم، استفاده خواهیم کرد. بنابراین این برنامه، از پیش تنظیم شدهاست و هم اکنون به همراه یک تولید کنندهی OpenAPI Specification نیز میباشد. آنرا اجرا کنید تا بتوان به OpenAPI Specification تولیدی آن در آدرس زیر دسترسی یافت:
https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json
مطابق تصویر، ابتدا آدرس Swagger Specification URL یا همان آدرس فوق را وارد کنید. سپس فضای نام دلخواهی را وارد کرده و گزینهی تولید کلاسهای کلاینت را فعلا انتخاب نکنید. در لیست تنظیمات آن، گزینهی Class Style نیز مهم است. برای مثال برای پروژههای ASP.NET Core حالت POCO را انتخاب کنید (plain old clr objects) و برای پروژههای مبتنی بر XAML، گزینهی Inpc مناسبتر است چون RaisePropertyChangedها را هم تولید میکند. در آخر بر روی دکمهی Generate Outputs کلیک کنید تا خروجی ذیل حاصل شود:
یا میتوان این خروجی را copy/paste کرد و یا میتوان در برگهی Settings، در انتهای لیست آن، مقدار output file path را مشخص کرد و سپس بر روی دکمهی Generate files کلیک نمود تا فایل معادل آن تولید شود.
استفاده از NSwagStudio برای تولید کدهای کلاینت Angular استفاده کنندهی از API
NSwagStudio امکان تولید یک TypeScript Client را نیز دارد:
در اینجا ابتدا TypeScript Client را انتخاب میکنیم و سپس در تنظیمات آن، قالب Angular را انتخاب کرده و نگارش RxJS آنرا نیز، 6 انتخاب میکنیم. در آخر بر روی Generate outputs کلیک میکنیم:
نکتهی جالب این خروجی، دقت داشتن به status codes درج شدهی در OpenAPI Spec است که در قسمتهای چهارم و پنجم سری «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger» آنها را بررسی کردیم.
در اینجا نه تنها سرویسی جهت تعامل با API ما تولید شدهاست، بلکه معادل تایپاسکریپتی DTOهای برنامه را نیز تولید کردهاست:
OpenCVSharp #9
در OpenCV با استفاده از مفهومی به نام affine transform، امکان تغییر اندازه و همچنین چرخش تصاویر میسر میشود. در اینجا، تصویر در یک ماتریس دو در سه ضرب میشود تا انتقالات یاد شده، انجام شوند.
private static void rotateImage(double angle, double scale, Mat src, Mat dst) { var imageCenter = new Point2f(src.Cols / 2f, src.Rows / 2f); var rotationMat = Cv2.GetRotationMatrix2D(imageCenter, angle, scale); Cv2.WarpAffine(src, dst, rotationMat, src.Size()); }
برای مشاهدهی بهتر تاثیر پارامترهای مختلف در اینجا، به مثال ذیل دقت کنید:
using OpenCvSharp; using OpenCvSharp.CPlusPlus; namespace OpenCVSharpSample09 { class Program { static void Main(string[] args) { using (var src = new Mat(@"..\..\Images\Penguin.Png", LoadMode.AnyDepth | LoadMode.AnyColor)) using (var dst = new Mat()) { src.CopyTo(dst); using (var window = new Window("Resize/Rotate/Blur", image: dst, flags: WindowMode.AutoSize)) { var angle = 0.0; var scale = 0.7; var angleTrackbar = window.CreateTrackbar( name: "Angle", value: 0, max: 180, callback: pos => { angle = pos; rotateImage(angle, scale, src, dst); window.Image = dst; }); var scaleTrackbar = window.CreateTrackbar( name: "Scale", value: 1, max: 10, callback: pos => { scale = pos / 10f; rotateImage(angle, scale, src, dst); window.Image = dst; }); angleTrackbar.Callback.DynamicInvoke(0); scaleTrackbar.Callback.DynamicInvoke(1); Cv2.WaitKey(); } } } private static void rotateImage(double angle, double scale, Mat src, Mat dst) { var imageCenter = new Point2f(src.Cols / 2f, src.Rows / 2f); var rotationMat = Cv2.GetRotationMatrix2D(imageCenter, angle, scale); Cv2.WarpAffine(src, dst, rotationMat, src.Size()); } } }
در این مثال، مانند مطلب قسمت قبل، ابتدا یک پنجرهی سازگار با C++ API ایجاد شده و سپس دو tracker به آن اضافه شدهاند. این trackers کار دریافت ورودی اطلاعات را از کاربر به عهده دارند (دریافت مقادیر زاویهی چرخش و مقیاس) و مقادیر دریافتی از آنها، در نهایت به متد rotateImage ارسال میشوند. این متد کار چرخش و تغییر مقیاس تصویر اصلی را انجام داده و نتیجه را به تصویر dst کپی میکند. در آخر تصویر dst در پنجره به روز شده و نمایش داده میشود.
تغییر اندازهی تصاویر
اگر صرفا قصد تغییر اندازهی تصاویر را دارید (بدون چرخش آنها)، متد ویژهای به نام Resize برای این منظور تدارک دیده شدهاست:
var resizeTrackbar = window.CreateTrackbar( name: "Resize", value: 1, max: 100, callback: pos => { Cv2.Resize(src, dst, new Size(src.Width + pos, src.Height + pos), interpolation: Interpolation.Cubic); window.Image = dst; });
اگر میخواهید مقادیر پارامترهای چرخشی تصویر نیز در اینجا اعمال شوند، میتوان به نحو ذیل عمل کرد:
var resizeTrackbar = window.CreateTrackbar( name: "Resize", value: 1, max: 100, callback: pos => { rotateImage(angle, scale, src, dst); Cv2.Resize(dst, dst, new Size(src.Width + pos, src.Height + pos), interpolation: Interpolation.Cubic); window.Image = dst; });
مات کردن تصاویر
در OpenCV با استفاده از متدهای GaussianBlur و یا medianBlur ، میتوان تصاویر را مات کرد که نمونهای از آنرا در ادامه ملاحظه میکنید:
var blurTrackbar = window.CreateTrackbar( name: "Blur", value: 1, max: 100, callback: pos => { if (pos % 2 == 0) pos++; rotateImage(angle, scale, src, dst); Cv2.GaussianBlur(dst, dst, new Size(pos, pos), sigmaX: 0); window.Image = dst; });
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
public class Page { public int Id { get; set; } public string Path { get; set; } public string Content { get; set; } }
public class CustomVirtualPathProvider : VirtualPathProvider { public override bool FileExists(string virtualPath) { return base.FileExists(virtualPath) || FileExistsInDatabase(virtualPath); } public override VirtualFile GetFile(string virtualPath) { return base.FileExists(virtualPath) ? base.GetFile(virtualPath) : new CustomVirtualFile(virtualPath, GetFileFromDatabase(virtualPath)); } private bool FileExistsInDatabase(string virtualPath) { virtualPath = virtualPath.Replace("~", ""); return new DatabaseContext().Pages.Any(v => v.Path == virtualPath); } private byte[] GetFileFromDatabase(string virtualPath) { virtualPath = virtualPath.Replace("~", ""); return Encoding.UTF8.GetBytes(new DatabaseContext().Pages.First(v => v.Path == virtualPath).Content); } }
public class CustomVirtualFile : VirtualFile { private readonly byte[] _content; public CustomVirtualFile(string virtualPath, byte[] content) : base(virtualPath) { _content = content; } public override Stream Open() { return new MemoryStream(_content); } }
حال نوبت ثبت کلاس CustomVirtualPathProvider جهت استفادهی خودکار از آن میباشد. برای این کار در تابع Application_Start موجود در فایل Global.asax.cs دستور زیر را اضافه مینماییم:
protected void Application_Start() { HostingEnvironment.RegisterVirtualPathProvider(new CustomVirtualPathProvider()); //... }
ASP.NET MVC #11
بررسی نکات تکمیلی Model binder در ASP.NET MVC
یک برنامه خالی جدید ASP.NET MVC را شروع کنید و سپس مدل زیر را به پوشه Models آن اضافه نمائید:
using System;
namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}
از این مدل چند مقصود ذیل دنبال میشوند:
استفاده از Id به عنوان primary key برای edit و update رکوردها. استفاده از DateTime برای اینکه اگر کاربری اطلاعات بی ربطی را وارد کرد چگونه باید این مشکل را در حالت model binding خودکار تشخیص داد و استفاده از IsAdmin برای یادآوری یک نکته امنیتی بسیار مهم که اگر حین model binding خودکار به آن توجه نشود، سایت را با مشکلات حاد امنیتی مواجه خواهد کرد. سیستم پیشرفته است. میتواند به صورت خودکار ورودیهای کاربر را تبدیل به یک شیء حاضر و آماده کند ... اما باید حین استفاده از این قابلیت دلپذیر به یک سری نکات امنیتی هم دقت داشت تا سایت ما به نحو دلپذیری هک نشود!
در ادامه یک کنترلر جدید به نام UserController را به پوشه کنترلرهای پروژه اضافه نمائید. همچنین نام کنترلر پیش فرض تعریف شده در قسمت مسیریابی فایل Global.asax.cs را هم به User تغییر دهید تا در هربار اجرای برنامه در VS.NET، نیازی به تایپ آدرسهای مرتبط با UserController نداشته باشیم.
یک منبع داده تشکیل شده در حافظه را هم برای نمایش لیستی از کاربران، به نحو زیر به پروژه اضافه خواهیم کرد:
using System;
using System.Collections.Generic;
namespace MvcApplication7.Models
{
public class Users
{
public IList<User> CreateInMemoryDataSource()
{
return new[]
{
new User { Id = 1, Name = "User1", Password = "123", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 2, Name = "User2", Password = "456", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 3, Name = "User3", Password = "789", IsAdmin = true, AddDate = DateTime.Now }
};
}
}
}
در اینجا فعلا هدف آشنایی با زیر ساختهای ASP.NET MVC است و درک صحیح نحوه کارکرد آن. مهم نیست از EF استفاده میکنید یا NH یا حتی ADO.NET کلاسیک و یا از Micro ORMهایی که پس از ارائه دات نت 4 مرسوم شدهاند. تهیه یک ToList یا Insert و Update با این فریم ورکها خارج از بحث جاری هستند.
سورس کامل کنترلر User به شرح زیر است:
using System;
using System.Linq;
using System.Web.Mvc;
using MvcApplication7.Models;
namespace MvcApplication7.Controllers
{
public class UserController : Controller
{
[HttpGet]
public ActionResult Index()
{
var usersList = new Users().CreateInMemoryDataSource();
return View(usersList); // Shows the Index view.
}
[HttpGet]
public ActionResult Details(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Details view.
}
[HttpGet]
public ActionResult Create()
{
var user = new User { AddDate = DateTime.Now };
return View(user); // Shows the Create view.
}
[HttpPost]
public ActionResult Create(User user)
{
if (this.ModelState.IsValid)
{
// todo: Add record
return RedirectToAction("Index");
}
return View(user); // Shows the Create view again.
}
[HttpGet]
public ActionResult Edit(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Edit view.
}
[HttpPost]
public ActionResult Edit(User user)
{
if (this.ModelState.IsValid)
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}
[HttpPost]
public ActionResult Delete(int id)
{
// todo: Delete record
return RedirectToAction("Index");
}
}
}
توضیحات:
ایجاد خودکار فرمهای ورود اطلاعات
در قسمت قبل برای توضیح دادن نحوه ایجاد فرمها در ASP.NET MVC و همچنین نحوه نگاشت اطلاعات آنها به اکشن متدهای کنترلرها، فرمهای مورد نظر را دستی ایجاد کردیم.
اما باید درنظر داشت که برای ایجاد Viewها میتوان از ابزار توکار خود VS.NET نیز استفاده کرد و سپس اطلاعات و فرمهای تولیدی را سفارشی نمود. این سریعترین راه ممکن است زمانیکه مدل مورد استفاده کاملا مشخص است و میخواهیم Strongly typed views را ایجاد کنیم.
برای نمونه بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. در اینجا گزینهی create a strongly typed view را انتخاب کرده و سپس از لیست مدلها، User را انتخاب نمائید. Scaffold template را هم بر روی حالت List قرار دهید.
برای متد Details هم به همین نحو عمل نمائید.
برای ایجاد View متناظر با متد Create در حالت HttpGet، تمام مراحل یکی است. فقط Scaffold template انتخابی را بر روی Create قرار دهید تا فرم ورود اطلاعات، به صورت خودکار تولید شود.
متد Create در حالت HttpPost نیازی به View اضافی ندارد. چون صرفا قرار است اطلاعاتی را از سرور دریافت و ثبت کند.
برای ایجاد View متناظر با متد Edit در حالت HttpGet، باز هم مراحل مانند قبل است با این تفاوت که Scaffold template انتخابی را بر روی گزینه Edit قرار دهید تا فرم ویرایش اطلاعات کاربر به صورت خودکار به پروژه اضافه شود.
متد Edit در حالت HttpPost نیازی به View اضافی ندارد و کارش تنها دریافت اطلاعات از سرور و به روز رسانی بانک اطلاعاتی است.
به همین ترتیب متد Delete نیز، نیازی به View خاصی ندارد. در اینجا بر اساس primary key دریافتی، میتوان یک کاربر را یافته و حذف کرد.
سفارشی سازی Viewهای خودکار تولیدی
با کمک امکانات Scaffolding نامبرده شده، حجم قابل توجهی کد را در اندک زمانی میتوان تولید کرد. بدیهی است حتما نیاز به سفارشی سازی کدهای تولیدی وجود خواهد داشت. مثلا شاید نیازی نباشد فیلد پسود کاربر، در حین نمایش لیست کاربران، نمایش داده شود. میشود کلا این ستون را حذف کرد و از این نوع مسایل.
یک مورد دیگر را هم در Viewهای تولیدی حتما نیاز است که ویرایش کنیم. آن هم مرتبط است به لینک حذف اطلاعات یک کاربر در صفحه Index.cshtml:
@Html.ActionLink("Delete", "Delete", new { id=item.Id }
در قسمت قبل هم عنوان شد که اعمال حذف باید بر اساس HttpPost محدود شوند تا بتوان میزان امنیت برنامه را بهبود داد. متد Delete هم در کنترلر فوق تنها به حالت HttpPost محدود شده است. بنابراین ActionLink پیش فرض را حذف کرده و بجای آن فرم و دکمه زیر را قرار میدهیم تا اطلاعات به سرور Post شوند:
@using (Html.BeginForm(actionName: "Delete", controllerName: "User", routeValues: new { id = item.Id }))
{
<input type="submit" value="Delete"
onclick="return confirm ('Do you want to delete this record?');" />
}
در اینجا نحوه ایجاد یک فرم، که id رکورد متناظر را به سرور ارسال میکند، مشاهده میکنید.
علت وجود دو متد، به ازای هر Edit یا Create
به ازای هر کدام از متدهای Edit و Create دو متد HttpGet و HttpPost را ایجاد کردهایم. کار متدهای HttpGet نمایش Viewهای متناظر به کاربر هستند. بنابراین وجود آنها ضروری است. در این حالت چون از دو Verb متفاوت استفاده شده، میتوان متدهای هم نامی را بدون مشکل استفاده کرد. به هر کدام از افعال Get و Post و امثال آن، یک Http Verb گفته میشود.
بررسی معتبر بودن اطلاعات دریافتی
کلاس پایه Controller که کنترلرهای برنامه از آن مشتق میشوند، شامل یک سری خواص و متدهای توکار نیز هست. برای مثال توسط خاصیت this.ModelState.IsValid میتوان بررسی کرد که آیا Model دریافتی معتبر است یا خیر. برای بررسی این مورد، یک breakpoint را بر روی سطر this.ModelState.IsValid در متد Create قرار دهید. سپس به صفحه ایجاد کاربر جدید مراجعه کرده و مثلا بجای تاریخ روز، abcd را وارد کنید. سپس فرم را به سرور ارسال نمائید. در این حالت مقدار خاصیت this.ModelState.IsValid مساوی false میباشد که حتما باید به آن پیش از ثبت اطلاعات دقت داشت.
شبیه سازی عملکرد ViewState در ASP.NET MVC
در متدهای Create و Edit در حالت Post، اگر اطلاعات Model معتبر نباشند، مجددا شیء User دریافتی، به View بازگشت داده میشود. چرا؟
صفحات وب، زمانیکه به سرور ارسال میشوند، تمام اطلاعات کنترلهای خود را از دست خواهد داد (صفحه پاک میشود، چون مجددا یک صفحه خالی از سرور دریافت خواهد شد). برای رفع این مشکل در ASP.NET Web forms، از مفهومی به نام ViewState کمک میگیرند. کار ViewState ذخیره موقت اطلاعات فرم جاری است برای استفاده مجدد پس از Postback. به این معنا که پس از ارسال فرم به سرور، اگر کاربری در textbox اول مقدار abc را وارد کرده بود، پس از نمایش مجدد فرم، مقدار abc را در همان textbox مشاهده خواهد کرد (شبیه سازی برنامههای دسکتاپ در محیط وب). بدیهی است وجود ViewState برای ذخیره سازی این نوع اطلاعات، حجم صفحه را بالا میبرد (بسته به پیچیدگی صفحه ممکن است به چند صد کیلوبایت هم برسد).
در ASP.NET MVC بجای استفاده از ترفندی به نام ViewState، مجددا اطلاعات همان مدل متناظر با View را بازگشت میدهند. در این حالت پس از ارسال صفحه به سرور و نمایش مجدد صفحه ورود اطلاعات، تمام کنترلها با همان مقادیر قبلی وارد شده توسط کاربر قابل مشاهده خواهند بود (مدل مشخص است، View ما هم از نوع strongly typed میباشد. در این حالت فریم ورک میداند که اطلاعات را چگونه به کنترلهای قرار گرفته در صفحه نگاشت کند).
در مثال فوق، اگر اطلاعات وارد شده صحیح باشند، کاربر به صفحه Index هدایت خواهد شد. در غیراینصورت مجددا همان View جاری با همان اطلاعات model قبلی که کاربر تکمیل کرده است به او برای تصحیح، نمایش داده میشود. این مساله هم جهت بالا بردن سهولت کاربری برنامه بسیار مهم است. تصور کنید که یک فرم خالی با پیغام «تاریخ وارد شده معتبر نیست» مجدا به کاربر نمایش داده شود و از او درخواست کنیم که تمام اطلاعات دیگر را نیز از صفر وارد کند چون اطلاعات صفحه پس از ارسال به سرور پاک شدهاند؛ که ... اصلا قابل قبول نیست و فوقالعاده برنامه را غیرحرفهای نمایش میدهد.
خطاهای نمایش داده شده به کاربر
به صورت پیش فرض خطایی که به کاربر نمایش داده میشود، استثنایی است که توسط فریم ورک صادر شده است. برای مثال نتوانسته است abcd را به یک تاریخ معتبر تبدیل کند. میتوان توسط this.ModelState.AddModelError خطایی را نیز در اینجا اضافه کرد و پیغام بهتری را به کاربر نمایش داد. یا توسط یک سری data annotations هم کار اعتبار سنجی را سفارشی کرد که بحث آن به صورت جداگانه در یک قسمت مستقل بررسی خواهد شد.
ولی به صورت خلاصه اگر به فرمهای تولید شده توسط VS.NET دقت کنید، در ابتدای هر فرم داریم:
@Html.ValidationSummary(true)
در اینجا خطاهای عمومی در سطح مدل نمایش داده میشوند. برای اضافه کردن این نوع خطاها، در متد AddModelError، مقدار key را خالی وارد کنید:
ModelState.AddModelError(string.Empty, "There is something wrong with model.");
همچنین در این فرمها داریم:
@Html.EditorFor(model => model.AddDate)
@Html.ValidationMessageFor(model => model.AddDate)
EditorFor سعی میکند اندکی هوش به خرج دهد. یعنی اگر خاصیت دریافتی مثلا از نوع bool بود، خودش یک checkbox را در صفحه نمایش میدهد. همچنین بر اساس متادیتا یک خاصیت نیز میتواند تصمیم گیری را انجام دهد. این متادیتا منظور attributes و data annotations ایی است که به خواص یک مدل اعمال میشود. مثلا اگر ویژگی HiddenInput را به یک خاصیت اعمال کنیم، به شکل یک فیلد مخفی در صفحه ظاهر خواهد شد.
یا متد Html.DisplayFor، اطلاعات را به صورت فقط خواندنی نمایش میدهد. اصطلاحا به این نوع متدها، Templated Helpers هم گفته میشود. بحث بیشتر دربارهای این موارد به قسمتی مجزا و مستقل موکول میگردد. برای نمونه کل فرم ادیت برنامه را حذف کنید و بجای آن بنویسید Html.EditorForModel و سپس برنامه را اجرا کنید. یک فرم کامل خودکار ویرایش اطلاعات را مشاهده خواهید کرد (و البته نکات سفارشی سازی آن به یک قسمت کامل نیاز دارند).
در اینجا متد ValidationMessageFor کار نمایش خطاهای اعتبارسنجی مرتبط با یک خاصیت مشخص را انجام میدهد. بنابراین اگر قصد ارائه خطایی سفارشی و مخصوص یک فیلد مشخص را داشتید، در متد AddModelError، مقدار پارامتر اول یا همان key را مساوی نام خاصیت مورد نظر قرار دهید.
مقابله با مشکل امنیتی Mass Assignment در حین کار با Model binders
استفاده از Model binders بسیار لذت بخش است. یک شیء را به عنوان پارامتر اکشن متد خود معرفی میکنیم. فریم ورک هم در ادامه سعی میکند تا اطلاعات فرم را به خواص این شیء نگاشت کند. بدیهی است این روش نسبت به روش ASP.NET Web forms که باید به ازای تک تک کنترلهای موجود در صفحه یکبار کار دریافت اطلاعات و مقدار دهی خواص یک شیء را انجام داد، بسیار سادهتر و سریعتر است.
اما اگر همین سیستم پیشرفته جدید ناآگاهانه مورد استفاده قرار گیرد میتواند منشاء حملات ناگواری شود که به نام «Mass Assignment» شهرت یافتهاند.
همان صفحه ویرایش اطلاعات را درنظر بگیرید. چک باکس IsAdmin قرار است در قسمت مدیریتی برنامه تنظیم شود. اگر کاربری نیاز داشته باشد اطلاعات خودش را ویرایش کند، مثلا پسوردش را تغییر دهد، با یک صفحه ساده کلمه عبور قبلی را وارد کنید و دوبار کلمه عبور جدید را نیز وارد نمائید، مواجه خواهد شد. خوب ... اگر همین کاربر صفحه را جعل کند و فیلد چک باکس IsAdmin را به صفحه اضافه کند چه اتفاقی خواهد افتاد؟ بله ... مشکل هم همینجا است. در اینصورت کاربر عادی میتواند دسترسی خودش را تا سطح ادمین بالا ببرد، چون model binder اطلاعات IsAdmin را از کاربر دریافت کرده و به صورت خودکار به model ارائه شده، نگاشت کرده است.
برای مقابله با این نوع حملات چندین روش وجود دارند:
الف) ایجاد لیست سفید
به کمک ویژگی Bind میتوان لیستی از خواص را جهت به روز رسانی به model binder معرفی کرد. مابقی ندید گرفته خواهند شد:
public ActionResult Edit([Bind(Include = "Name, Password")] User user)
در اینجا تنها خواص Name و Password توسط model binder به خواص شیء User نگاشت میشوند.
به علاوه همانطور که در قسمت قبل نیز ذکر شد، متد edit را به شکل زیر نیز میتوان بازنویسی کرد. در اینجا متدهای توکار UpdateModel و TryUpdateModel نیز لیست سفید خواص مورد نظر را میپذیرند (اعمال دستی model binding):
[HttpPost]
public ActionResult Edit()
{
var user = new User();
if(TryUpdateModel(user, includeProperties: new[] { "Name", "Password" }))
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}
ب) ایجاد لیست سیاه
به همین ترتیب میتوان تنها خواصی را معرفی کرد که باید صرفنظر شوند:
public ActionResult Edit([Bind(Exclude = "IsAdmin")] User user)
در اینجا از خاصیت IsAdmin صرف نظر گردیده و از مقدار ارسالی آن توسط کاربر استفاده نخواهد شد.
و یا میتوان پارامتر excludeProperties متد TryUpdateModel را نیز مقدار دهی کرد.
لازم به ذکر است که ویژگی Bind را به کل یک کلاس هم میتوان اعمال کرد. برای مثال:
using System;
using System.Web.Mvc;
namespace MvcApplication7.Models
{
[Bind(Exclude = "IsAdmin")]
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}
این مورد اثر سراسری داشته و قابل بازنویسی نیست. به عبارتی حتی اگر در متدی خاصیت IsAdmin را مجددا الحاق کنیم، تاثیری نخواهد داشت.
یا میتوان از ویژگی ReadOnly هم استفاده کرد:
using System;
using System.ComponentModel;
namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
[ReadOnly(true)]
public bool IsAdmin { set; get; }
}
}
در این حالت هم خاصیت IsAdmin هیچگاه توسط model binder به روز و مقدار دهی نخواهد شد.
ج) استفاده از ViewModels
این راه حلی است که بیشتر مورد توجه معماران نرم افزار است و البته کسانی که پیشتر با الگوی MVVM کار کرده باشند این نام برایشان آشنا است؛ اما در اینجا مفهوم متفاوتی دارد. در الگوی MVVM، کلاسهای ViewModel شبیه به کنترلرها در MVC هستند یا به عبارتی همانند رهبر یک اکستر عمل میکنند. اما در الگوی MVC خیر. در اینجا فقط مدل یک View هستند و نه بیشتر. هدف هم این است که بین Domain Model و View Model تفاوت قائل شد.
کار View model در الگوی MVC، شکل دادن به چندین domain model و همچنین اطلاعات اضافی دیگری که نیاز هستند، جهت استفاده نهایی توسط یک View میباشد. به این ترتیب View با یک شیء سر و کار خواهد داشت و همچنین منطق شکل دهی به اطلاعات مورد نیازش هم از داخل View حذف شده و به خواص View model در زمان تشکیل آن منتقل میشود.
مشخصات یک View model خوب به شرح زیر است:
الف) رابطه بین یک View و View model آن، رابطهای یک به یک است. به ازای هر View، بهتر است یک کلاس View model وجود داشته باشد.
ب) View ساختار View model را دیکته میکند و نه کنترلر.
ج) View modelها صرفا یک سری کلاس POCO (کلاسهایی تشکیل شده از خاصیت، خاصیت، خاصیت ....) هستند که هیچ منطقی در آنها قرار نمیگیرد.
د) View model باید حاوی تمام اطلاعاتی باشد که View جهت رندر نیاز دارد و نه بیشتر و الزامی هم ندارد که این اطلاعات مستقیما به domain models مرتبط شوند. برای مثال اگر قرار است firstName+LastName در View نمایش داده شود، کار این جمع زدن باید حین تهیه View Model انجام شود و نه داخل View. یا اگر قرار است اطلاعات عددی با سه رقم جدا کننده به کاربر نمایش داده شوند، وظیفه View Model است که یک خاصیت اضافی را برای تهیه این مورد تدارک ببیند. یا مثلا اگر یک فرم ثبت نام داریم و در این فرم لیستی وجود دارد که تنها Id عنصر انتخابی آن در Model اصلی مورد استفاده قرار میگیرد، تهیه اطلاعات این لیست هم کار ViewModel است و نه اینکه مدام به Model اصلی بخواهیم خاصیت اضافه کنیم.
ViewModel چگونه پیاده سازی میشود؟
اکثر مقالات را که مطالعه کنید، این روش را توصیه میکنند:
public class MyViewModel
{
public SomeDomainModel1 Model1 { get; set; }
public SomeDomainModel2 Model2 { get; set; }
...
}
یعنی اینکه View ما به اطلاعات مثلا دو Model نیاز دارد. اینها را به این شکل محصور و کپسوله میکنیم. اگر View، واقعا به تمام فیلدهای این کلاسها نیاز داشته باشد، این روش صحیح است. در غیر اینصورت، این روش نادرست است (و متاسفانه همه جا هم دقیقا به این شکل تبلیغ میشود).
ViewModel محصور کننده یک یا چند مدل نیست. در اینجا حس غلط کار کردن با یک ViewModel را داریم. ViewModel فقط باید ارائه کننده اطلاعاتی باشد که یک View نیاز دارد و نه بیشتر و نه تمام خواص تمام کلاسهای تعریف شده. به عبارتی این نوع تعریف صحیح است:
public class MyViewModel
{
public string SomeExtraField1 { get; set; }
public string SomeExtraField2 { get; set; }
public IEnumerable<SelectListItem> StateSelectList { get; set; }
// ...
public string PersonFullName { set; set; }
}
در اینجا، View متناظری، قرار است نام کامل یک شخص را به علاوه یک سری اطلاعات اضافی که در domain model نیست، نمایش دهد. مثلا نمایش نام استانها که نهایتا Id انتخابی آن قرار است در برنامه استفاده شود.
خلاصه علت وجودی ViewModel این موارد است:
الف) Model برنامه را مستقیما در معرض استفاده قرار ندهیم (عدم رعایت این نکته به مشکلات امنیتی حادی هم حین به روز رسانی اطلاعات ممکن است ختم شود که پیشتر توضیح داده شد).
ب) فیلدهای نمایشی اضافی مورد نیاز یک View را داخل Model برنامه تعریف نکنیم (مثلا تعاریف عناصر یک دراپ داون لیست، جایش اینجا نیست. مدل فقط نیاز به Id عنصر انتخابی آن دارد).
با این توضیحات، اگر View به روز رسانی اطلاعات کلمه عبور کاربر، تنها به اطلاعات id آن کاربر و کلمه عبور او نیاز دارد، فقط باید همین اطلاعات را در اختیار View قرار داد و نه بیشتر:
namespace MvcApplication7.Models
{
public class UserViewModel
{
public int Id { set; get; }
public string Password { set; get; }
}
}
به این ترتیب دیگر خاصیت IsAdming اضافهای وجود ندارد که بخواهد مورد حمله واقع شود.
استفاده از model binding برای آپلود فایل به سرور
برای آپلود فایل به سرور تنها کافی است یک اکشن متد به شکل زیر را تعریف کنیم. HttpPostedFileBase نیز یکی دیگر از model binderهای توکار ASP.NET MVC است:
[HttpGet]
public ActionResult Upload()
{
return View(); // Shows the upload page
}
[HttpPost]
public ActionResult Upload(System.Web.HttpPostedFileBase file)
{
string filename = Server.MapPath("~/files/somename.ext");
file.SaveAs(filename);
return RedirectToAction("Index");
}
View متناظر هم میتواند به شکل زیر باشد:
@{
ViewBag.Title = "Upload";
}
<h2>
Upload</h2>
@using (Html.BeginForm(actionName: "Upload", controllerName: "User",
method: FormMethod.Post,
htmlAttributes: new { enctype = "multipart/form-data" }))
{
<text>Upload a photo:</text> <input type="file" name="photo" />
<input type="submit" value="Upload" />
}
اگر دقت کرده باشید در طراحی ASP.NET MVC از anonymously typed objects زیاد استفاده میشود. در اینجا هم برای معرفی enctype فرم آپلود، مورد استفاده قرار گرفته است. به عبارتی هر جایی که مشخص نبوده چه تعداد ویژگی یا کلا چه ویژگیها و خاصیتهایی را میتوان تنظیم کرد، اجازه تعریف آنها را به صورت anonymously typed objects میسر کردهاند. یک نمونه دیگر آن در متد routes.MapRoute فایل Global.asax.cs است که پارامتر سوم دریافت مقدار پیش فرضها نیز anonymously typed object است. یا نمونه دیگر آنرا در همین قسمت در جایی که لینک delete را به فرم تبدیل کردیم مشاهده نمودید. مقدار routeValues هم یک anonymously typed object معرفی شد.
سفارشی سازی model binder پیش فرض ASP.NET MVC
در همین مثال فرض کنید تاریخ را به صورت شمسی از کاربر دریافت میکنیم. خاصیت تعریف شده هم DateTime میلادی است. به عبارتی model binder حین تبدیل رشته تاریخ شمسی دریافتی به تاریخ میلادی با شکست مواجه شده و نهایتا خاصیت this.ModelState.IsValid مقدارش false خواهد بود. برای حل این مشکل چکار باید کرد؟
برای این منظور باید نحوه پردازش یک نوع خاص را سفارشی کرد. ابتدا با پیاده سازی اینترفیس IModelBinder شروع میکنیم. توسط bindingContext.ValueProvider میتوان به مقداری که کاربر وارد کرده در میانه راه دسترسی یافت. آنرا تبدیل کرده و نمونه صحیح را بازگشت داد.
نمونهای از این پیاده سازی را در ادامه ملاحظه میکنید:
using System;
using System.Globalization;
using System.Web.Mvc;
namespace MvcApplication7.Binders
{
public class PersianDateModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
var parts = valueResult.AttemptedValue.Split('/'); //ex. 1391/1/19
if (parts.Length != 3) return null;
int year = int.Parse(parts[0]);
int month = int.Parse(parts[1]);
int day = int.Parse(parts[2]);
actualValue = new DateTime(year, month, day, new PersianCalendar());
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}
سپس برای معرفی PersianDateModelBinder جدید تنها کافی است سطر زیر را
ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());
به متد Application_Start قرار گرفته در فایل Global.asax.cs برنامه اضافه کرد. از این پس کاربران میتوانند تاریخها را در برنامه شمسی وارد کنند و model binder بدون مشکل خواهد توانست اطلاعات ورودی را به معادل DateTime میلادی آن تبدیل کند و استفاده نماید.
تعریف مدل بایندر سفارشی در فایل Global.asax.cs آنرا به صورت سراسری در تمام مدلها و اکشنمتدها فعال خواهد کرد. اگر نیاز بود تنها یک اکشن متد خاص از این مدل بایندر سفارشی استفاده کند میتوان به روش زیر عمل کرد:
public ActionResult Create([ModelBinder(typeof(PersianDateModelBinder))] User user)
همچنین ویژگی ModelBinder را به یک کلاس هم میتوان اعمال کرد:
[ModelBinder(typeof(PersianDateModelBinder))]
public class User
{
مروری بر نحوهی کارکرد مسیریابی اصلی برنامه
به router-outlet ایی که در فایل قالب src\app\app.component.html قرار گرفتهاست، primary outlet میگویند. زمانیکه کاربر، برنامه را در مرورگر مشاهده میکند، با هربار کلیک بر روی یکی از لینکهای منوی بالای سایت، قالب آنرا در این primary outlet مشاهده میکند. اگر بخواهیم پنل دیگری را در همین صفحه و در همین سطح از نمایش، درج کنیم، نیاز به تعریف outlet دیگری است که به همراه مسیرهای ثانویهای نیز خواهد بود.
تعریف یک router-outlet نامدار
با توجه به اینکه هر پنل به همراه مسیریابی ثانویه، نیاز به router-outlet خودش را خواهد داشت، مسیریاب برای اینکه بداند محتوای آنها را در کجای صفحه درج کند، به نامهای آنها مراجعه میکند. به این ترتیب میتوان چندین router-outlet را در یک سطح از نمایش تعریف کرد؛ اما هرکدام باید دارای نامی منحصربفرد باشند.
در مثال این سری میخواهیم پنلی را در سمت راست صفحهی اصلی درج کنیم. برای تعریف آن در همان سطحی که router-outlet اصلی قرار دارد، نیاز است فایل src\app\app.component.html را ویرایش کنیم:
<div class="container"> <div class="row"> <div class="col-md-10"> <router-outlet></router-outlet> </div> <div class="col-md-2"> <router-outlet name="popup"></router-outlet> </div> </div> </div>
افزودن ماژول جدید پیامهای سیستم
در ادامه ماژول جدید پیامهای سیستم را به همراه تنظیمات ابتدایی مسیریابی آن اضافه خواهیم کرد که در آن ماژول، مدیریت نمایش پیامهای مختلفی در router-outlet ثانویه popup صورت خواهد گرفت:
>ng g m message --routing
در ادامه نیاز است MessageModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم (پیش از AppRoutingModule که حاوی مسیریابی catch all است):
import { MessageModule } from './message/message.module'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }), ProductModule, UserModule, MessageModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
سپس کامپوننت جدید Message را به ماژول Message برنامه اضافه میکنیم:
>ng g c message/message
پس از آن یک سرویس ابتدایی پیامهای کاربران را نیز اضافه خواهیم کرد:
>ng g s message/message -m message/message.module
installing service create src\app\message\message.service.spec.ts create src\app\message\message.service.ts update src\app\message\message.module.ts
پس از ایجاد قالب ابتدایی فایل message.service.ts آنرا به نحو ذیل تکمیل میکنیم:
import { Injectable } from '@angular/core'; @Injectable() export class MessageService { private messages: string[] = []; isDisplayed = false; addMessage(message: string): void { let currentDate = new Date(); this.messages.unshift(message + ' at ' + currentDate.toLocaleString()); } }
اکنون جهت تکمیل کامپوننت پیامها، ابتدا فایل قالب message.component.html را به نحو ذیل تکمیل میکنیم:
<div class="row"> <h4 class="col-md-10">Message Log</h4> <span class="col-md-2"> <a class="btn btn-default" (click)="close()">x</a> </span> </div> <div *ngFor="let message of messageService.messages; let i=index"> <div *ngIf="i<10" class="message-row"> {{ message }} </div> </div>
کدهای کامپوننت این قالب به صورت ذیل است:
import { MessageService } from './../message.service'; import { Router } from '@angular/router'; import { Component, OnInit } from '@angular/core'; @Component({ //selector: 'app-message', templateUrl: './message.component.html', styleUrls: ['./message.component.css'] }) export class MessageComponent implements OnInit { constructor(private messageService: MessageService, private router: Router) { } ngOnInit() { } close(): void { // Close the popup. this.router.navigate([{ outlets: { popup: null } }]); this.messageService.isDisplayed = false; } }
تکمیل سایر کامپوننتهای برنامه در جهت استفاده از سرویس پیامها
ابتدا به فایل src\app\product\product-edit\product-edit.component.ts مراجعه کرده و سرویس جدید پیامها را به سازندهی آن تزریق میکنیم:
import { MessageService } from './../../message/message.service'; @Component({ selector: 'app-product-edit', templateUrl: './product-edit.component.html', styleUrls: ['./product-edit.component.css'] }) export class ProductEditComponent implements OnInit { constructor(private productService: ProductService, private messageService: MessageService, private route: ActivatedRoute, private router: Router) { }
onSaveComplete(message?: string): void { if (message) { this.messageService.addMessage(message); }
تنظیم مسیرهای ثانویه
نحوهی تعریف مسیریابیهای مرتبط با router-outletهای غیراصلی برنامه، همانند سایر مسیریابیهای برنامهاست؛ با این تفاوت که در اینجا خاصیت outlet نیز به تنظیمات مسیر اضافه خواهد شد. به این ترتیب مشخص خواهیم کرد که محتوای این مسیر باید دقیقا در کدام router-outlet نامدار، درج شود.
برای این منظور فایل src\app\message\message-routing.module.ts را گشوده و تنظیمات مسیریابی آنرا که به صورت RouterModule.forChild تعریف میشوند (چون ماژول اصلی برنامه نیستند)، تکمیل خواهیم کرد:
const routes: Routes = [ { path: 'messages', component: MessageComponent, outlet: 'popup' } ];
فعالسازی یک مسیر ثانویه
در اینجا نیز همانند سایر مسیریابیها، از دایرکتیو routerLink برای فعالسازی مسیرهای ثانویه استفاده میکنیم؛ اما syntax آن کمی متفاوت است:
<a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Messages</a> <a [routerLink]="['/products', product.id, 'edit', { outlets: { popup: ['summary', product.id] } }]">Messages</a>
در دومین لینک تعریف شده، ابتدا یک مسیر اصلی فعال شده و سپس یک مسیر ثانویه نمایش داده میشود.
یک نکته: هرچند به primary outlet نامی انتساب داده نمیشود، اما نام آن دقیقا primary است و میتوان قسمت outlets را به صورت ذیل نیز تعریف کرد:
{ outlets: { primary: ['/products', product.id,'edit'], popup: ['summary', product.id] }}
در ادامه فایل src\app\app.component.html را ویرایش کرده و لینک Show Messages را به آن اضافه میکنیم:
<ul class="nav navbar-nav navbar-right"> <li *ngIf="authService.isLoggedIn()"> <a>Welcome {{ authService.currentUser.userName }}</a> </li> <li> <a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Show Messages</a> </li>
آدرس آن نیز چنین شکلی را پیدا میکند:
http://localhost:4200/products(popup:messages)
اکنون میخواهیم قابلیت مخفی سازی این پنل را نیز پیاده سازی کنیم. به همین جهت از خاصیت isDisplayed سرویس پیامها که توسط دکمهی بستن MessageComponent مدیریت میشود، استفاده خواهیم کرد. بنابراین لینک جدیدی را که در فایل src\app\app.component.html اضافه کردیم، به نحو ذیل تغییر خواهیم داد:
<li *ngIf="!messageService.isDisplayed"> <a (click)="displayMessages()">Show Messages</a> </li> <li *ngIf="messageService.isDisplayed"> <a (click)="hideMessages()">Hide Messages</a> </li>
import { MessageService } from './message/message.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(private authService: AuthService, private router: Router, private messageService: MessageService) { } displayMessages(): void { this.router.navigate([{ outlets: { popup: ['messages'] } }]); this.messageService.isDisplayed = true; } hideMessages(): void { this.router.navigate([{ outlets: { popup: null } }]); this.messageService.isDisplayed = false; } }
برای فعالسازی یک مسیرثانویه توسط متدهای برنامه، نیاز است از سرویس مسیریاب و متد navigate آن استفاده کرد که نمونههایی از آنرا در اینجا ملاحظه میکنید. پارامترهای ذکر شدهی در اینجا نیز همانند دایرکتیو routerLink هستند.
یک نکته: اگر به متد hideMessages دقت کنید، مقدار value کلید popup به نال تنظیم شدهاست. این مورد سبب خواهد شد تا outlet آن خالی شود. به این ترتیب متد hideMessages علاوه بر مخفی کردن لینک نمایش پیامها، پنل آنرا نیز از صفحه حذف میکند. شبیه به همین نکته در متد close کامپوننت پیامها که دکمهی بستن آنرا به همراه دارد، پیاده سازی شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
بررسی روش آپلود فایلها در ASP.NET Core
اما ... این قالب تهیه شده، صرفا بر اساس یکی از چندین پروژه Solution جاری تهیه میشود و همچنین نصب و توزیع آن نیز دستی است. در ادامه قصد داریم با نحوه تهیه یک قالب جدید پروژه متشکل از چندین پروژه، به همراه تهیه فایل VSI نصاب آن، آشنا شویم.
تهیه یک ساختار نمونه
یک پروژه جدید کنسول را به نام MyConsoleApplication ایجاد کنید. سپس به Solution جاری، یک Class library جدید را به نام مثلا MyConsoleApplication.Tests اضافه نمائید. تا اینجا به شکل زیر خواهیم رسید:
اکنون قصد داریم از این پروژه خاص، یک قالب تهیه کنیم؛ تا هربار نخواهیم یک چنین مراحلی را تکرار کنیم.
تهیه قالب به ازای هر پروژه در Solution
در همین حال که Solution باز است، به منوی File و گزینه Export template مراجعه کنید.
در اینجا تنها امکان انتخاب یک پروژه وجود دارد. به همین جهت این مرحله را باید به ازای هر تعداد پروژه موجود در Solution یکبار تکرار کرد.
اکنون در پوشه My Documents\Visual Studio 2010\My Exported Templates دو فایل zip به نامهای MyConsoleApplication.zip و MyConsoleApplication.Tests.zip وجود دارند. هر دو فایل را توسط برنامههای مخصوص گشودن فایلهای Zip گشوده و تبدیل به دو پوشه باز شده MyConsoleApplication و MyConsoleApplication.Tests کنید.
افزودن فایل MyTemplate.vstemplate چند پروژهای
در همین پوشه جاری که اکنون حاوی دو پوشه باز شده است، یک فایل متنی جدید را با محتوای ذیل به نام MyTemplate.vstemplate ایجاد کنید:
<VSTemplate Version="3.0.0" Type="ProjectGroup" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005"> <TemplateData> <Name>MyConsoleApplication</Name> <Description>MyConsoleApplication Desc</Description> <ProjectType>CSharp</ProjectType> </TemplateData> <TemplateContent> <ProjectCollection> <ProjectTemplateLink ProjectName="MyConsoleApplication"> MyConsoleApplication\MyTemplate.vstemplate</ProjectTemplateLink> <ProjectTemplateLink ProjectName="MyConsoleApplication.Tests"> MyConsoleApplication.Tests\MyTemplate.vstemplate</ProjectTemplateLink> </ProjectCollection> </TemplateContent> </VSTemplate>
در ادامه این دو پوشه باز شده و فایل MyTemplate.vstemplate فوق را انتخاب کرده:
و همگی را تبدیل به یک فایل zip جدید کنید؛ مثلا به نام MyConsoleApplicationTemplates.zip.
تهیه فایل نصاب از قالب پروژه جدید
تا اینجا موفق شدیم، چندین قالب پروژه تهیه شده را به هم متصل کرده و تبدیل به یک فایل zip نهایی کنیم. مرحله بعد ایجاد فایلی است متنی به نام MyConsoleApplicationTemplates.vscontent با محتویات زیر:
<VSContent xmlns="http://schemas.microsoft.com/developer/vscontent/2005"> <Content> <FileName>MyConsoleApplicationTemplates.zip</FileName> <DisplayName>MyConsoleApplication</DisplayName> <Description>A C# project that ...</Description> <FileContentType>VSTemplate</FileContentType> <ContentVersion>1.0</ContentVersion> <Attributes> <Attribute name="ProjectType" value="Visual C#" /> <Attribute name="ProjectSubType" value="Web" /> <Attribute name="TemplateType" value="Project" /> </Attributes> </Content> </VSContent>
مرحله بعد انتخاب دو فایل MyConsoleApplicationTemplates.vscontent و MyConsoleApplicationTemplates.zip و تبدیل ایندو به یک فایل zip جدید است. پس از ایجاد فایل جدید، پسوند آنرا به VSI تغییر دهید؛ برای مثال نام آنرا به MyConsoleApplicationTemplates.vsi تغییر دهید. اکنون این فایل نهایی با دوبار کلیک بر روی آن قابلیت اجرا و نصب خودکار را پیدا میکند.
پس از نصب، بلافاصله ذیل قسمت پروژههای وب قابل دسترسی و استفاده خواهد بود:
بنابراین به صورت خلاصه:
1) به ازای هر پروژه، یک فایل قالب zip معادل آن باید تهیه شود.
2) تمام این فایلهای zip را گشوده و تبدیل به پوشههای متناظری کنید.
3) یک فایل MyTemplate.vstemplate را در پوشه ریشه مرحله 2 جهت تعریف ProjectTemplateLinkها اضافه کنید.
4) فایل جدید MyTemplate.vstemplate مرحله 3 و تمام پوشههای قالبهای باز شده مرحله 2 را zip کنید.
5) سپس یک فایل vscontent نصاب را تهیه و آنرا با فایل zip مرحله 4 مجددا zip کرده و پسوند آنرا به VSI تغییر دهید.
اکنون میتوان این فایل VSI را توزیع کرد.