import {Book} from "./testmd";
ایجاد یک برنامهی Minimal-API جدید در دات نت 8
پروژهای را که در اینجا پیگیری میکنیم، بر اساس قالب استاندارد تولید شدهی توسط دستور dotnet new webapi تکمیل میشود.
ایجاد یک صفحهی Blazor 8x به همراه مسیریابی و دریافت پارامتر
در ادامه قصد داریم که یک کامپوننت جدید را به نام SsrTest.razor در پوشهی جدید Components\Tests ایجاد کرده و برای آن مسیریابی از نوع page@ هم تعریف کنیم. یعنی نهفقط قصد داریم آنرا توسط RazorComponentResult رندر کنیم، بلکه میخواهیم اگر آدرس آنرا در مرورگر هم وارد کردیم، قابل دسترسی باشد.
به همین جهت یک پوشهی جدید را به نام Components در ریشهی پروژهی Web API جاری ایجاد میکنیم، با این محتوا:
برای ایده گرفتن از محتوای مورد نیاز، به «معرفی قالبهای جدید شروع پروژههای Blazor در دات نت 8» قسمت دوم این سری مراجعه کرده و برای مثال قالب سادهترین حالت ممکن را توسط دستور زیر تولید میکنیم (در یک پروژهی مجزا، خارج از پروژهی جاری):
dotnet new blazor --interactivity None
- فایل Imports.razor_ ساده شده برای سهولت کار با فضاهای نام در کامپوننتهای Blazor (فضاهای نامی را که در آن وجود ندارند و مرتبط با پروژهی دوم هستند، حذف میکنیم).
- فایل App.razor، برای تشکیل نقطهی آغازین برنامهی Blazor.
- فایل Routes.razor برای معرفی مسیریابی صفحات Blazor تعریف شده.
- پوشهی Layout برای معرفی فایل MainLayout.razor که در Routes.razor استفاده شدهاست.
و ... یک فایل آزمایشی جدید به نام Components\Tests\SsrTest.razor با محتوای زیر:
@page "/ssr-page/{Data:int}" <PageTitle>An SSR component</PageTitle> <h1>An SSR component rendered by a Minimal-API!</h1> <div> Data: @Data </div> @code { [Parameter] public int Data { get; set; } }
تغییرات مورد نیاز در فایل Program.cs برنامهی Web-API برای فعالسازی رندر سمت سرور Blazor
در ادامه کل تغییرات مورد نیاز جهت اجرای این برنامه را مشاهده میکنید:
var builder = WebApplication.CreateBuilder(args); // ... builder.Services.AddRazorComponents(); // ... // http://localhost:5227/ssr-component?data=2 // or it can be called directly http://localhost:5227/ssr-page/2 app.MapGet("/ssr-component", (int data = 1) => { var parameters = new Dictionary<string, object?> { { nameof(SsrTest.Data), data }, }; return new RazorComponentResult<SsrTest>(parameters); }); app.UseStaticFiles(); app.UseAntiforgery(); app.MapRazorComponents<App>(); app.Run(); // ...
- همین اندازه تغییر در جهت فعالسازی رندر سمت سرور کامپوننتهای Blazor در یک برنامهی ASP.NET Core کفایت میکند. یعنی اضافه شدن:
AddRazorComponents ،UseAntiforgery و MapRazorComponents
- در اینجا نحوهی ارسال پارامترها را به یک RazorComponentResult نیز مشاهده میکنید.
- در حالت فراخوانی از طریق مسیر endpoint (یعنی فراخوانی مسیر http://localhost:5227/ssr-component در مثال فوق)، خود کامپوننت فراخوانی شده، بدون layout تعریف شدهی در فایل App.razor، رندر میشود. علت اینجا است که layout برنامه به همراه کامپوننت Router و RouteView آن فعال میشود که این دو هم مختص به صفحات دارای مسیریابی Blazor هستند و برای رندر کامپوننتهای خالص آن بکار گرفته نمیشوند. خروجی RazorComponentResult تنها یک static SSR خالص است؛ مگر اینکه فایل blazor.web.js را نیز بارگذاری کند.
یک نکته: اگر در حالت رندر توسط RazorComponentResult، علاقمند به استفادهی از layout هستید، میتوان از کامپوننت LayoutView داخل یک کامپوننت فرضی به صورت زیر استفاده کرد؛ اما این مورد هم شامل اطلاعات فایل App.razor نمیشود:
<LayoutView Layout="@typeof(MainLayout)"> <PageTitle>Home</PageTitle> <h2>Welcome to your new app.</h2> </LayoutView>
سؤال: آیا در این حالت کامپوننتهای تعاملی هم کار میکنند؟
پاسخ: بله. فقط برای ایده گرفتن، یک نمونه پروژهی تعاملی Blazor 8x را در ابتدا ایجاد کنید و قسمتهای اضافی AddRazorComponents و MapRazorComponents آنرا در اینجا کپی کنید؛ یعنی برای مثال جهت فعالسازی کامپوننتهای تعاملی Blazor Server، به این دو تغییر زیر نیاز است:
// ... builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); // ... app.MapRazorComponents<App>().AddInteractiveServerRenderMode(); // ...
<script src="_framework/blazor.web.js"></script>
@rendermode InteractiveServer <h1>Counter</h1> <p role="status">Current count: @_currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int _currentCount; private void IncrementCount() { _currentCount++; } }
// http://localhost:5227/server-interactive-component app.MapGet("/server-interactive-component", () => new RazorComponentResult<Counter>());
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive server component</title> <base href="/"/> </head> <body> <h1>Interactive server component</h1> <Counter/> <script src="_framework/blazor.web.js"></script> </body> </html>
سپس تعریف endpoint متناظر را به صورت زیر تغییر میدهیم:
// http://localhost:5227/server-interactive-component app.MapGet("/server-interactive-component", () => new RazorComponentResult<CounterInteractive>());
سؤال: آیا میتوان این خروجی static SSR کامپوننتهای بلیزر را در سرویسهای یک برنامه ASP.NET Core هم دریافت کرد؟
منظور این است که آیا میتوان از یک کامپوننت Blazor، به همراه تمام پیشرفتهای Razor در آن که در Viewهای MVC قابل دسترسی نیستند، بهشکل یک رشتهی خالص، خروجی گرفت و برای مثال از آن بهعنوان قالب پویای محتوای ایمیلها استفاده کرد؟
پاسخ: بله! زیر ساخت RazorComponentResult که از سرویس HtmlRenderer استفاده میکند، بدون نیاز به برپایی یک endpoint هم قابل دسترسی است:
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; namespace WebApi8x.Services; public class BlazorStaticRendererService { private readonly HtmlRenderer _htmlRenderer; public BlazorStaticRendererService(HtmlRenderer htmlRenderer) => _htmlRenderer = htmlRenderer; public Task<string> StaticRenderComponentAsync<T>() where T : IComponent => RenderComponentAsync<T>(ParameterView.Empty); public Task<string> StaticRenderComponentAsync<T>(Dictionary<string, object?> dictionary) where T : IComponent => RenderComponentAsync<T>(ParameterView.FromDictionary(dictionary)); private Task<string> RenderComponentAsync<T>(ParameterView parameters) where T : IComponent => _htmlRenderer.Dispatcher.InvokeAsync(async () => { var output = await _htmlRenderer.RenderComponentAsync<T>(parameters); return output.ToHtmlString(); }); }
builder.Services.AddScoped<HtmlRenderer>(); builder.Services.AddScoped<BlazorStaticRendererService>();
app.MapGet("/static-renderer-service-test", async (BlazorStaticRendererService renderer, int data = 1) => { var parameters = new Dictionary<string, object?> { { nameof(SsrTest.Data), data }, }; var html = await renderer.StaticRenderComponentAsync<SsrTest>(parameters); return Results.Content(html, "text/html"); });
کدهای کامل این مطلب را میتوانید از اینجا دریافت کنید: WebApi8x.zip
در مطلب جاری قصد داریم با نحوه ارائه یک UI کاربر پسند برای این منظور آشنا شویم و سؤال مهم هم این است: «چگونه میتوان کار کاربر را در حین وارد کردن تعدادی از برچسبهای مرتبط با یک مطلب سادهتر کرد؟». برای این منظور یکی از راه حلهایی که در بسیاری از سایتها مرسوم شده است، استفاده از افزونههایی مانند jQuery TagIt میباشد که در ادامه با نحوه استفاده از آن در ASP.NET MVC آشنا خواهیم شد.
پیشنیازها:
دریافت افزونه TagIt
همچنین دریافت jQuery UI (افزونه TagIt برای نمایش لیست Auto Complete آیتمها از jQuery UI در پشت صحنه استفاده میکند)
<head> <title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/TagIt/jquery-ui-1.8.23.custom.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("~/Content/TagIt/tagit-simple-blue.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("Content/Site.css")" rel="stylesheet" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Content/TagIt/jquery-ui-1.8.23.custom.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Content/TagIt/tagit.js")" type="text/javascript"></script> @RenderSection("JavaScript", required: false) </head>
آشنایی با مدل برنامه
using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace jQueryMvcSample04.Models { public class BlogPostViewModel { [DisplayName("عنوان"), Required(ErrorMessage = "*")] public string Title { set; get; } [DisplayName("متن"), Required(ErrorMessage = "*")] public string Body { set; get; } /// <summary> /// آرایهای محدود از برچسبهای این مطلب خاص به صورت جیسون که پیشتر ثبت شده است /// هدف استفاده در حین ویرایش مطلب /// </summary> public string InitialTags { set; get; } /// <summary> /// آرایهای جیسونی از تمام برچسبهای موجود در سیستم /// هدف نمایش منوی انتخاب برچسبها از لیست /// </summary> public string TagsSource { set; get; } /// <summary> /// آرایهای از برچسبهای وارد شده توسط کاربر در حین ثبت مطلب /// </summary> [DisplayName("برچسبها"), Required(ErrorMessage = "*")] public string[] Tags { set; get; } public int? Id { set; get; } } }
افزونه TagIt برای نمایش اطلاعات خود به دو منبع داده نیاز دارد:
الف) TagsSource : لیستی است به فرمت JSON، از هر آنچه که در سیستم پیشتر به عنوان یک برچسب ثبت شده است. از این لیست برای نمایش منوی خودکار انتخاب آیتمها استفاده میشود.
ب) InitialTags : لیستی است به فرمت JSON، از تمام برچسبهای مرتبط با یک مطلب. از این اطلاعات در حین ویرایش یک مطلب استفاده خواهد شد.
در این ViewModel یک خاصیت دیگر به شکل آرایه، به نام Tags تعریف شده است که لیست برچسبهای وارد شده توسط کاربر را دریافت خواهد کرد.
معرفی کنترلر برنامه
using System.Web.Mvc; using jQueryMvcSample04.Extensions; using jQueryMvcSample04.Models; namespace jQueryMvcSample04.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index(int? id) { //در ابتدای کار تمام تگهای موجود در سیستم از بانک اطلاعاتی دریافت خواهند شد //از این تگها برای تشکیل منوی انتخاب برچسبها استفاده میشود var tagsSource = new[] { "C#", "C++", "C", "ASP.NET", "MVC" }.ToJson(); //همچنین صرفا برچسبهای مطلب جاری که پیشتر ثبت شدهاند نیز باید از بانک اطلاعاتی دریافت گردند //از این برچسبها برای ویرایش یک مطلب موجود استفاده خواهد شد var init = new[] { "ASP.NET" }.ToJson(); var model = new BlogPostViewModel { TagsSource = tagsSource, InitialTags = init, Id = id }; return View(model); } [HttpPost] public ActionResult Index(BlogPostViewModel data) { if (this.ModelState.IsValid) { //todo: save data // ... return RedirectToAction(actionName: "index", controllerName: "home"); } //در صورت بروز خطا مجددا اطلاعات موجود نمایش داده خواهند شد data.TagsSource = new[] { "C#", "C++", "C", "ASP.NET", "MVC" }.ToJson(); data.InitialTags = data.Tags.ToJson(); return View(data); } } }
با توجه به توضیحاتی که ارائه شد، کنترلر برنامه ساختار واضحتری را یافته است. در اولین بار نمایش صفحه، لیست منبع داده تگها و همچنین تگهای مرتبط با یک مطلب (در صورت وجود) به View ارائه خواهند شد.
از همین ViewModel، در عملیات Post نیز استفاده گردیده و اطلاعات دریافت میگردد.
تعریف متد الحاقی ToJson مورد استفاده را نیز در ادامه ملاحظه مینمائید:
using System.Linq; using System.Web.Script.Serialization; namespace jQueryMvcSample04.Extensions { public static class JsonExt { public static string ToJson(this string[] initialTags) { if (initialTags == null || !initialTags.Any()) return "[]"; else return new JavaScriptSerializer().Serialize(initialTags); } } }
و مرحله آخر تعریف View متناظر است
@model jQueryMvcSample04.Models.BlogPostViewModel @{ ViewBag.Title = "Index"; } @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>ثبت مطلب جدید</legend> @Html.HiddenFor(model => model.Id) <div class="editor-label"> @Html.LabelFor(model => model.Title) </div> <div class="editor-field"> @Html.EditorFor(model => model.Title) @Html.ValidationMessageFor(model => model.Title) </div> <div class="editor-label"> @Html.LabelFor(model => model.Body) </div> <div class="editor-field"> @Html.EditorFor(model => model.Body) @Html.ValidationMessageFor(model => model.Body) </div> <div class="editor-label"> @Html.LabelFor(model => model.Tags) </div> <div class="editor-field"> <ul id="tagsList" dir="ltr" name="Tags"> </ul> @Html.ValidationMessageFor(model => model.Tags) </div> <p> <input type="submit" value="Create" /> </p> </fieldset> } @section JavaScript { <script type="text/javascript"> $(document).ready(function () { var tagsSource = @Html.Raw(Model.TagsSource); $('#tagsList').tagit({ tagSource: tagsSource, select: true, triggerKeys: ['enter', 'comma', 'tab'], initialTags: @Html.Raw(Model.InitialTags) }); }); </script> }
الف) برای نمایش افزونه TagIt از یک ul با id مساوی tagsList استفاده شده است.
ب) خواص اضافی موجود در ViewModel که اطلاعات JSON ایی مورد نیاز را بازگشت میدهند در قسمت اسکریپت صفحه مورد استفاده قرار گرفتهاند. در اینجا نیاز است از Html.Raw استفاده شود تا اطلاعات مرتبط با JSON اشتباها encode نشده و قابل استفاده باشند.
دریافت مثال و پروژه کامل این قسمت
jQueryMvcSample04.zip
نگاهی به SignalR Clients
مصرف کنندگان یک Hub میتوانند انواع و اقسام برنامههای کلاینت مانند jQuery Clients و یا حتی یک برنامه کنسول ساده باشند و همچنین Hubهای دیگر نیز قابلیت استفاده از این امکانات Hubهای موجود را دارند. تیم SignalR امکان استفاده از Hubهای آنرا در برنامههای دات نت 4 به بعد، برنامههای WinRT، ویندوز فون 8، سیلورلایت 5، jQuery و همچنین برنامههای CPP نیز مهیا کردهاند. به علاوه گروههای مختلف نیز با توجه به سورس باز بودن این مجموعه، کلاینتهای iOS Native، iOS via Mono و Android via Mono را نیز به این لیست اضافه کردهاند.
بررسی کلاینتهای jQuery
با توجه به پروتکل مبتنی بر JSON سیگنالآر، استفاده از آن در کتابخانههای جاوا اسکریپتی همانند jQuery نیز به سادگی مهیا است. برای نصب آن نیاز است در کنسول پاور شل نوگت، دستور زیر را صادر کنید:
PM> Install-Package Microsoft.AspNet.SignalR.JS
با استفاده از افزونه SignalR jQuery، به دو طریق میتوان به یک Hub اتصال برقرار کرد:
الف) استفاده از فایل proxy تولید شده آن (این فایل، در زمان اجرای برنامه تولید میشود و یا امکان استفاده از آن به کمک ابزارهای کمکی نیز وجود دارد)
نمونهای از آنرا در قسمت قبل ملاحظه کردید؛ همان فایل تولید شده در مسیر /signalr/hubs برنامه. به نوعی به آن Service contract نیز گفته میشود (ارائه متادیتا و قراردادهای کار با یک سرویس Hub). این فایل همانطور که عنوان شد به صورت پویا در زمان اجرای برنامه ایجاد میشود.
امکان تولید آن توسط برنامه کمکی signalr.exe نیز وجود دارد؛ برای دریافت آن میتوان از طریق NuGet اقدام کرد (بسته Microsoft.AspNet.SignalR.Utils) که نهایتا در پوشه packages قرار خواهد گرفت. نحوه استفاده از آن نیز به صورت زیر است:
Signalr.exe ghp http://localhost/
ب) بدون استفاده از فایل proxy و به کمک روش late binding (انقیاد دیر هنگام)
برای کار با یک Hub از طریق jQuery مراحل ذیل باید طی شوند:
1) ارجاعی به Hub باید مشخص شود.
2) روالهای رخدادگردان تنظیم گردند.
3) اتصال به Hub برقرار گردد.
4) متدی فراخوانی شود.
در اینجا باید دقت داشت که امکانات Hub به صورت خواص
$.connection
$.connection.chatHub
خوب، تا اینجا فرض بر این است که یک پروژه خالی ASP.NET را آغاز و سپس فرمان نصب Microsoft.AspNet.SignalR.JS را نیز همانطور که عنوان شد، صادر کردهاید. در ادامه یک فایل ساده html را به نام chat.htm، به این پروژه جدید اضافه کنید (برای استفاده از کتابخانه جاوا اسکریپتی SignalR الزامی به استفاده از صفحات کامل پروژههای وب نیست).
<!DOCTYPE> <html> <head> <title></title> <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script> <script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script> </head> <body> <div> <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" /> <ul id="messages"> </ul> </div> <script type="text/javascript"> $(function () { var chat; $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ میکند chat = $.connection.chat; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است chat.client.hello = function (message) { //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است //به این ترتیب سرور میتواند کلاینت را فراخوانی کند $("#messages").append("<li>" + message + "</li>"); }; $.connection.hub.start(/*{ transport: 'longPolling' }*/); // فاز اولیه ارتباط را آغاز میکند $("#send").click(function () { // Hub's `SendMessage` should be camel case here chat.server.sendMessage($("#txtMsg").val()); }); }); </script> </body> </html>
توضیحات:
همانطور که ملاحظه میکنید ابتدا ارجاعاتی به jquery و jquery.signalR-1.0.1.min.js اضافه شدهاند. سپس نیاز است مسیر دقیق فایل پروکسی هاب خود را نیز مشخص کنیم. اینکار با تعریف مسیر signalr/hubs انجام شده است.
<script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
سپس ارجاعی به هاب تعریف شده، تعریف گردیده است. اگر از قسمت قبل به خاطر داشته باشید، توسط ویژگی HubName، نام chat را برگزیدیم. بنابراین connection.chat ذکر شده دقیقا به این هاب اشاره میکند.
سپس سطر chat.client.hello مقدار دهی شده است. متد hello، متدی dynamic و تعریف شده در سمت هاب برنامه است. به این ترتیب میتوان به پیامهای رسیده از طرف سرور گوش فرا داد. در اینجا، این پیامها، به li ایی با id مساوی messages اضافه میشوند.
سپس توسط فراخوانی متد connection.hub.start، فاز negotiation شروع میشود. در اینجا حتی میتوان نوع transport را نیز صریحا انتخاب کرد که نمونهای از آن را به صورت کامنت شده جهت آشنایی با نحوه تعریف آن مشاهده میکنید. مقادیر قابل استفاده در آن به شرح زیر هستند:
- webSockets - forverFrame - serverSentEvents - longPolling
اکنون به صورت جداگانه یکبار برنامه hub را در مرورگر باز کنید. سپس بر روی فایل chat.htm کلیک راست کرده و گزینه مشاهده آن را در مرورگر نیز انتخاب نمائید (گزینه View in browser منوی کلیک راست).
خوب! پروژه کار نمیکند! برای اینکه مشکلات را بهتر بتوانید مشاهده کنید نیاز است به JavaScript Console مرورگر خود مراجعه نمائید. برای مثال در مرورگر کروم دکمه F12 را فشرده و برگه Console آنرا باز کنید. در اینجا اعلام میکند که فاز negotiation قابل انجام نیست؛ چون مسیر پیش فرضی را که انتخاب کرده است، همین مسیر پروژه دومی است که اضافه کردهایم (کلاینت ما در پروژه دوم قرار دارد و نه در همان پروژه اول هاب).
برای اینکه مسیر دقیق hub را در این حالت مشخص کنیم، سطر زیر را به ابتدای کدهای جاوا اسکریپتی فوق اضافه نمائید:
$.connection.hub.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
using System; using System.Web; using System.Web.Routing; using Microsoft.AspNet.SignalR; namespace SignalR02 { public class Global : HttpApplication { protected void Application_Start(object sender, EventArgs e) { // Register the default hubs route: ~/signalr RouteTable.Routes.MapHubs(new HubConfiguration { EnableCrossDomain = true }); } } }
SignalR: Auto detected cross domain url. jquery.signalR-1.0.1.min.js:10 SignalR: Negotiating with 'http://localhost:1072/signalr/negotiate'. jquery.signalR-1.0.1.min.js:10 SignalR: SignalR: Initializing long polling connection with server. jquery.signalR-1.0.1.min.js:10 SignalR: Attempting to connect to 'http://localhost:1072/signalr/connect?transport=longPolling&connectionToken…NRh72omzsPkKqhKw2&connectionData=%5B%7B%22name%22%3A%22chat%22%7D%5D&tid=3' using longPolling. jquery.signalR-1.0.1.min.js:10 SignalR: Longpolling connected jquery.signalR-1.0.1.min.js:10
در برگه شبکه، مطابق شکل فوق، امکان آنالیز اطلاعات رد و بدل شده مهیا است. برای مثال در حالتیکه سرور پیام دریافتی را به کلیه کلاینتها ارسال میکند، نام متد و نام هاب و سایر پارامترها در اطلاعات به فرمت JSON آن به خوبی قابل مشاهده هستند.
یک نکته:
اگر از ویندوز 8 (یعنی IIS8) و VS 2012 استفاده میکنید، برای استفاده از حالت Web socket، ابتدا فایل وب کانفیگ برنامه را باز کرده و در قسمت httpRunTime، مقدار ویژگی targetFramework را بر روی 4.5 تنظیم کنید. اینبار اگر مراحل negotiation را بررسی کنید در همان مرحله اول برقراری اتصال، از روش Web socket استفاده گردیده است.
تمرین 1
به پروژه ساده و ابتدایی فوق یک تکست باکس دیگر به نام Room را اضافه کنید؛ به همراه دکمه join. سپس نکات قسمت قبل را در مورد الحاق به یک گروه و سپس ارسال پیام به اعضای گروه را پیاده سازی نمائید. (تمام نکات آن با مطلب فوق پوشش داده شده است و در اینجا باید صرفا فراخوانی متدهای عمومی دیگری در سمت هاب، صورت گیرد)
تمرین 2
در انتهای قسمت دوم به نحوه ارسال پیام از یک هاب به هابی دیگر اشاره شد. این MonitorHub را ایجاد کرده و همچنین یک کلاینت جاوا اسکریپتی را نیز برای آن تهیه کنید تا بتوان اتصال و قطع اتصال کلیه کاربران سیستم را مانیتور و مشاهده کرد.
پیاده سازی کلاینت jQuery بدون استفاده از کلاس Proxy
در مثال قبل، از پروکسی پویای مهیای در آدرس signalr/hubs استفاده کردیم. در اینجا قصد داریم، بدون استفاده از آن نیز کار برپایی کلاینت را بررسی کنیم.
بنابراین یک فایل جدید html را مثلا به نام chat_np.html به پروژه دوم برنامه اضافه کنید. سپس محتویات آنرا به نحو زیر تغییر دهید:
<!DOCTYPE> <html> <head> <title></title> <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script> </head> <body> <div> <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" /> <ul id="messages"> </ul> </div> <script type="text/javascript"> $(function () { $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ میکند var connection = $.hubConnection(); connection.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم var proxy = connection.createHubProxy('chat'); proxy.on('hello', function (message) { //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است //به این ترتیب سرور میتواند کلاینت را فراخوانی کند $("#messages").append("<li>" + message + "</li>"); }); $("#send").click(function () { // Hub's `SendMessage` should be camel case here proxy.invoke('sendMessage', $("#txtMsg").val()); }); connection.start(); }); </script> </body> </html>
کلاینتهای دات نتی SignalR
تا کنون Solution ما حاوی یک پروژه Hub و یک پروژه وب کلاینت جیکوئری است. به همین Solution، یک پروژه کلاینت کنسول ویندوزی را نیز اضافه کنید.
سپس در خط فرمان پاور شل نوگت دستور زیر را صادر نمائید تا فایلهای مورد نیاز به پروژه کنسول اضافه شوند:
PM> Install-Package Microsoft.AspNet.SignalR.Client
پس از نصب آن اگر به پوشه packages مراجعه کنید، نگارشهای مختلف آنرا مخصوص سیلورلایت، دات نتهای 4 و 4.5، WinRT و ویندوز فون8 نیز میتوانید در پوشه Microsoft.AspNet.SignalR.Client ملاحظه نمائید. البته در ابتدای نصب، انتخاب نگارش مناسب، بر اساس نوع پروژه جاری به صورت خودکار صورت میگیرد.
مدل برنامه نویسی آن نیز بسیار شبیه است به حالت عدم استفاده از پروکسی در حین استفاده از jQuery که در قسمت قبل بررسی گردید و شامل این مراحل است:
1) یک وهله از شیء HubConnection را ایجاد کنید.
2) پروکسی مورد نیاز را جهت اتصال به Hub از طریق متد CreateProxy تهیه کنید.
3) رویدادگردانها را همانند نمونه کدهای جاوا اسکریپتی قسمت قبل، توسط متد On تعریف کنید.
4) به کمک متد Start، اتصال را آغاز نمائید.
5) متدها را به کمک متد Invoke فراخوانی نمائید.
using System; using Microsoft.AspNet.SignalR.Client.Hubs; namespace SignalR02.WinClient { class Program { static void Main(string[] args) { var hubConnection = new HubConnection(url: "http://localhost:1072/signalr"); var chat = hubConnection.CreateHubProxy(hubName: "chat"); chat.On<string>("hello", msg => { Console.WriteLine(msg); }); hubConnection.Start().Wait(); chat.Invoke<string>("sendMessage", "Hello!"); Console.WriteLine("Press a key to terminate the client..."); Console.Read(); } } }
نکته مهم
کلیه فراخوانیهایی که در اینجا ملاحظه میکنید غیرهمزمان هستند.
به همین جهت پس از متد Start، متد Wait ذکر شدهاست تا در این برنامه ساده، پس از برقراری کامل اتصال، کار invoke صورت گیرد و یا زمانیکه callback تعریف شده توسط متد chat.On فراخوانی میشود نیز این فراخوانی غیرهمزمان است و خصوصا اگر نیاز است رابط کاربری برنامه را در این بین به روز کنید باید به نکات به روز رسانی رابط کاربری از طریق یک ترد دیگر دقت داشت.
فعال سازی پردازش فایلهای استاتیک در برنامههای ASP.NET Core 1.0
در مورد پوشهی جدید wwwroot در «قسمت 2 - بررسی ساختار جدید Solution» مطالبی عنوان شدند. جهت یادآوری:
اگر فایل Program.cs را بررسی کنید، یک چنین تعاریفی را مشاهده خواهید کرد:
public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .Build(); host.Run(); } }
یک مثال: زمانیکه فایل استاتیک images/banner3.svg در پوشهی wwwroot قرار میگیرد، با آدرس http://localhost:9189/images/banner3.svg توسط عموم قابل دسترسی خواهد بود.
یک نکتهی امنیتی مهم
در برنامههای ASP.NET Core، هنوز فایل web.config را نیز مشاهده میکنید. این فایل تنها کاربردی که در اینجا دارد، تنظیم ماژول AspNetCoreModule برای IIS است تا IIS static file handler آن، راسا اقدام به توزیع فایلهای یک برنامهی ASP.NET Core نکند. بنابراین توزیع این فایل را بر روی سرورهای IIS فراموش نکنید. همچنین بهتر است در ویندوزهای سرور، به قسمت Modules feature مراجعه کرده و StaticFileModule را از لیست ویژگیهای موجود حذف کرد.
نصب Middleware مخصوص پردازش فایلهای استاتیک
در قسمت قبل با نحوهی نصب و فعال سازی middleware مخصوص WelcomePage آشنا شدیم. روال کار در اینجا نیز دقیقا به همان صورت است:
الف) نصب بستهی نیوگت Microsoft.AspNetCore.StaticFiles
برای اینکار میتوان بر روی گرهی references کلیک راست کرده و سپس از منوی ظاهر شده،گزینهی manage nuget packages را انتخاب کرد. سپس ابتدا برگهی browse را انتخاب کنید و در اینجا نام Microsoft.AspNetCore.StaticFiles را جستجو کرده و سپس نصب کنید.
انجام این کارها معادل افزودن یک سطر ذیل به فایل project.json است و سپس ذخیرهی آن که کار بازیابی بستهها را به صورت خودکار آغاز میکند:
"dependencies": { // same as before "Microsoft.AspNetCore.StaticFiles": "1.0.0" },
برای اینکار به فایل Startup.cs مراجعه کرده و سطر UseStaticFiles را به متد Configure اضافه کنید (به UseWelcomePage هم دیگر نیازی نداریم):
public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app) { app.UseStaticFiles(); //app.UseWelcomePage(); app.Run(async context => { await context.Response.WriteAsync("Hello DNT!"); }); } }
یک مثال: بر روی پوشهی wwwroot کلیک راست کرده و گزینهی add->new item را انتخاب کنید. سپس یک HTML page جدید را به نام index.html به این پوشه اضافه کنید.
با این محتوا:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Hello World</title> </head> <body> Hello World! </body> </html>
که این خروجی دقیقا خروجی app.Run برنامه است و نه محتوای فایل index.html ایی که اضافه کردیم.
در ادامه اگر مسیر کامل این فایل را (http://localhost:7742/index.html) درخواست دهیم، آنگاه میتوان خروجی این فایل استاتیک را مشاهده کرد:
این رفتار اندکی متفاوت است نسبت به نگارشهای قبلی ASP.NET که فایل index.html را به عنوان فایل پیش فرض، درنظر میگرفت و محتوای آنرا نمایش میداد. منظور از فایل پیش فرض، فایلی است که با درخواست ریشهی یک مسیر، به کاربر ارائه داده میشود و index.html یکی از آنها است.
برای رفع این مشکل، نیاز است Middleware مخصوص آنرا به نام Default Files نیز به برنامه معرفی کرد:
public void Configure(IApplicationBuilder app) { app.UseDefaultFiles(); app.UseStaticFiles();
فعال سازی Default Files، سبب جستجوی یکی از 4 فایل ذیل به صورت پیش فرض میشود (اگر تنها ریشهی پوشهای درخواست شود):
default.htm
default.html
index.htm
index.html
اگر خواستید فایل سفارشی خاص دیگری را معرفی کنید، نیاز است پارامتر DefaultFilesOptions آنرا مقدار دهی نمائید:
// Serve my app-specific default file, if present. DefaultFilesOptions options = new DefaultFilesOptions(); options.DefaultFileNames.Clear(); options.DefaultFileNames.Add("mydefault.html"); app.UseDefaultFiles(options);
ترتیب معرفی Middlewares مهم است
در قسمت قبل، در حین معرفی تفاوتهای Middlewareها با HTTP Modules، عنوان شد که اینبار برنامه نویس میتواند بر روی ترتیب اجرای Middlewareها کنترل کاملی داشته باشد و این ترتیب معادل است با ترتیب معرفی آنها در متد Configure، به نحوی که مشاهده میکنید. برای آزمایش این مطلب، متد معرفی middleware فایلهای پیش فرض را پس از متد معرفی فایلهای استاتیک قرار دهید:
public void Configure(IApplicationBuilder app) { app.UseStaticFiles(); app.UseDefaultFiles();
بله. اینبار تعریف فایلهای پیش فرض، هیچ تاثیری نداشته و درخواست ریشهی سایت، بدون ذکر صریح نام فایلی، مجددا به app.Run ختم شدهاست.
توزیع فایلهای استاتیک خارج از wwwroot
همانطور که در ابتدای بحث عنوان شد، با فعال سازی UseStaticFiles به صورت پیش فرض مسیر content root/wwwroot در معرض دید دنیای خارج قرار میگیرد و توسط وب سرور قابل توزیع خواهد شد:
○ wwwroot § css § images § ... ○ MyStaticFiles § test.png
برای این منظور میتوان از پارامتر StaticFileOptions متد UseStaticFiles به نحو ذیل جهت معرفی پوشهی MyStaticFiles استفاده کرد:
app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")), RequestPath = new PathString("/StaticFiles") });
فعال سازی مشاهدهی مرور فایلهای استاتیک بر روی سرور
فرض کنید پوشهی تصاویر را به پوشهی عمومی wwwroot اضافه کردهایم. برای فعال سازی مرور محتوای این پوشه میتوان از Middleware دیگری به نام DirectoryBrowser استفاده کرد:
app.UseDirectoryBrowser(new DirectoryBrowserOptions { FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")), RequestPath = new PathString("/MyImages") });
Unable to resolve service for type 'System.Text.Encodings.Web.HtmlEncoder' while attempting to activate 'Microsoft.AspNetCore.StaticFiles.DirectoryBrowserMiddleware'.
public void ConfigureServices(IServiceCollection services) { services.AddDirectoryBrowser(); }
مشکل! در این حالت که DirectoryBrowser را فعال کردهایم، اگر بر روی لینک فایل تصویر نمایش داده شده کلیک کنیم، باز پیام Hello DNT یا اجرای app.Run را شاهد خواهیم بود.
به این دلیل که UseStaticFiles پیش فرض، مسیر درخواستی MyImages را که بر روی file system وجود ندارد، نمیشناسد. برای رفع این مشکل تنها کافی است مسیریابی این Request Path خاص را نیز فعال کنیم:
app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")), RequestPath = new PathString("/MyImages") });
بررسی خلاصهی تنظیماتی که به فایل آغازین برنامه اضافه شدند
تا اینجا اگر توضیحات را قدم به قدم دنبال و اجرا کرده باشید، یک چنین تنظیماتی را خواهید داشت:
using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; namespace Core1RtmEmptyTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddDirectoryBrowser(); } public void Configure(IApplicationBuilder app) { app.UseDefaultFiles(); app.UseStaticFiles(); // For the wwwroot folder // For the files outside of the wwwroot app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")), RequestPath = new PathString("/StaticFiles") }); // For DirectoryBrowser app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")), RequestPath = new PathString("/MyImages") }); app.UseDirectoryBrowser(new DirectoryBrowserOptions { FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")), RequestPath = new PathString("/MyImages") }); //app.UseWelcomePage(); app.Run(async context => { await context.Response.WriteAsync("Hello DNT!"); }); } } }
UseDefaultFiles کار فعال سازی شناسایی فایلهای پیش فرضی مانند index.html را در صورت ذکر نام ریشهی یک پوشه، انجام میدهد.
اولین UseStaticFiles تعریف شده، تمام مسیرهای فیزیکی ذیل wwwroot را عمومی میکند.
دومین UseStaticFiles تعریف شده، پوشهی MyStaticFiles واقع در خارج از wwwroot را عمومی میکند.
سومین UseStaticFiles تعریف شده، پوشهی فیزیکی wwwroot\images را به مسیر درخواستهای MyImages نگاشت میکند (http://localhost:7742/myimages) تا توسط DirectoryBrowser تعریف شده، قابل استفاده شود.
در آخر هم DirectoryBrowser تعریف شدهاست.
یک نکتهی امنیتی مهم
یک چنین قابلیتی (مرور فایلهای درون یک پوشه) به صورت پیش فرض بر روی تمام IISها به دلایل امنیتی غیرفعال است. به همین جهت بهتر است Middleware فوق را هیچگاه استفاده نکنید و به این قسمت صرفا از دیدگاه اطلاعات عمومی نگاه کنید.
ساده سازی تعاریف توزیع فایلهای استاتیک
Middleware دیگری به نام FileServer کار تعریف توزیع فایلهای استاتیک را ساده میکند. اگر آنرا تعریف کنید:
app.UseFileServer();
اگر خواستید DirectoryBrowsing آنرا نیز فعال کنید، پارامتر ورودی آنرا به true مقدار دهی کنید (که به صورت پیش فرض غیرفعال است):
app.UseFileServer(enableDirectoryBrowsing: true);
app.UseFileServer(new FileServerOptions { FileProvider = new PhysicalFileProvider( Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")), RequestPath = new PathString("/StaticFiles"), EnableDirectoryBrowsing = false });
توزیع فایلهای ناشناخته
اگر به سورس ASP.NET Core 1.0 دقت کنید، کلاسی را به نام FileExtensionContentTypeProvider خواهید یافت. اینها پسوندها و mime typeهای متناظری هستند که توسط ASP.NET Core شناخته شده و توزیع میشوند. برای مثال اگر فایلی را به نام test.xyz به پوشهی wwwroot اضافه کنید، درخواست آن توسط کاربر، به Hello DNT ختم میشود؛ چون در این کلاس پایه، پسوند xyz تعریف نشدهاست.
برای رفع این مشکل و تکمیل این لیست میتوان به نحو ذیل عمل کرد:
// Set up custom content types -associating file extension to MIME type var provider = new FileExtensionContentTypeProvider(); provider.Mappings[".xyz"] = "text/html"; app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider }) ; // For the wwwroot folder
و یا اگر خواستید کمی تمیزتر کار کنید، بهتر است از کلاس پایه FileExtensionContentTypeProvider ارث بری کرده و سپس در سازندهی این کلاس، خاصیت Mappings را ویرایش نمود:
public class XyzContentTypeProvider : FileExtensionContentTypeProvider { public XyzContentTypeProvider() { this.Mappings.Add(".xyz", "text/html"); } }
app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = new XyzContentTypeProvider() }) ; // For the wwwroot folder
روش دیگر مدیریت این مساله، تنظیم مقدار خاصیت ServeUnknownFileTypes به true است:
app.UseStaticFiles(new StaticFileOptions { ServeUnknownFileTypes = true, DefaultContentType = "image/png" });
Media Type یا MIME Type نشان دهنده فرمت یک مجموعه داده است. در HTTP، مدیا تایپ بیان کننده فرمت message body یک درخواست / پاسخ است و به دریافت کننده اعلام میکند که چطور باید پیام را بخواند. محل استاندارد تعیین Mime Type در هدر Content-Type است. درخواست کننده میتواند با استفاده از هدر Accept لیستی از MimeTypeهای قابل قبول را به عنوان پاسخ، به سرور اعلام کند.
Asp.net Web API از MimeType برای تعیین نحوه serialize یا deserialize کردن محتوای دریافتی / ارسالی استفاده میکند
MediaTypeFormatter
Web API برای خواندن/درج پیام در بدنه درخواست/پاسخ از MediaTypeFormmaterها استفاده میکند. اینها کلاسهایی هستند که نحوهی Serialize کردن و deserialize کردن اطلاعات به فرمتهای خاص را تعیین میکنند. Web API به صورت توکار دارای formatter هایی برای نوعهای XML ، JSON، BSON و Form-UrlEncoded میباشد. همه اینها کلاس پایه MediaTypeFormatter را پیاده سازی میکنند.
مسئله
یک پروژه Web API بسازید و view model زیر را در آن تعریف کنید:
public class NewProduct { [Required] public string Name { get; set; } public double Price { get; set; } public byte[] Pic { get; set; } }
همانطور که میبینید یک فیلد از نوع byte[] برای تصویر محصول در نظر گرفته شده است.
حالا یک کنترلر API ساخته و اکشنی برای دریافت اطلاعات محصول جدید از کاربر مینویسیم :public class ProductsController : ApiController { [HttpPost] public HttpResponseMessage PostProduct(NewProduct model) { if (ModelState.IsValid) { // ثبت محصول return new HttpResponseMessage(HttpStatusCode.Created); } return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); } }
و یک صفحه html به نام index.html که حاوی یک فرم برای ارسال اطلاعات باشد :
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <h1>ساخت MediaTypeFormatter برای Multipart/form-data</h1> <h2>محصول جدید</h2> <form id="newProduct" method="post" action="/api/products" enctype="multipart/form-data"> <div> <label for="name">نام محصول : </label> <input type="text" id="name" name="name" /> </div> <div> <label for="price">قیمت : </label> <input type="number" id="price" name="price" /> </div> <div> <label for="pic">تصویر : </label> <input type="file" id="pic" name="pic" /> </div> <div> <button type="submit">ثبت</button> </div> </form> </body> </html>
زمانی که فرم حاوی فایلی برای آپلود باشد مشخصه encType باید برابر با Multipart/form-data مقداردهی شود تا اطلاعات فایل به درستی کد شوند. در زمان ارسال فرم Content-type درخواست برابر با Multipart/form-data و فرمت اطلاعات درخواست ارسالی به شکل زیر خواهد بود :
همانطور که میبینید هر فیلد در فرم، در یک بخش جداگانه قرار گرفته است که با خط چین هایی از هم جدا شده اند. هر بخش، headerهای جداگانه خود را دارد.
- Content-Disposition که نام فیلد و نام فایل را شامل میشود .
- content-type که mime type مخصوص آن بخش از دادهها را مشخص میکند.
پس از اینکه فرم را تکمیل کرده و ارسال کنید ، با پیام خطای زیر مواجه میشوید :
خطای روی داده اعلام میکند که Web API فاقد MediaTypeFormatter برای خواندن اطلاعات ارسال شده با فرمتMultiPart/Form-data است. Web API برای خواندن و بایند کردن پارامترهای complex Type از درون بدنه پیام یک درخواست از MediaTypeFormatter استفاده میکند و همانطور که گفته شد Web API فاقد Formatter توکار برای deserialize کردن دادههای با فرمت Multipart/form-data است.
راه حلها :روشی که در سایت asp.net برای آپلود فایل در web api استفاده شده، عدم استفاده از پارامترها و خواندن محتوای Request در درون کنترلر است. که به طبع در صورتی که بخواهیم کنترلرهای تمیز و کوچکی داشته باشیم روش مناسبی نیست. از طرفی امتیاز parameter binding و modelstate را هم از دست خواهیم داد.
روش دیگری که میخواهیم در اینجا پیاده سازی کنیم ساختن یک MediaTypeFormatter برای خواندن فرمت Multipart/form-data است. با این روش کد موردنیاز کپسوله شده و امکان استفاده از binding و modelstate را خواهیم داشت.
برای ساختن یک MediaTypeFormatter یکی از 2 کلاس MediaTypeFormatter یا BufferedMediaTypeFormatter را باید پیاده سازی کنیم . تفاوت این دو در این است که BufferedMediaTypeFormatter برخلاف MediaTypeFormatter از متدهای synchronous استفاده میکند.
یک کلاس به نام MultiPartMediaTypeFormatter میسازیم و کلاس MediaTypeFormatter را به عنوان کلاس پایه آن قرار میدهیم .
public class MultiPartMediaTypeFormatter : MediaTypeFormatter { ... }
ابتدا در تابع سازنده کلاس فرمت هایی که میخواهیم توسط این کلاس خوانده شوند را تعریف میکنیم :
public MultiPartMediaTypeFormatter() { SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data")); }
سپس با پیاده سازی توابع CanReadType و CanWriteType مربوط به کلاس MediaTypeFormatter مشخص میکنیم که چه مدلهایی را میتوان توسط این کلاس serialize / deserialize کرد. در اینجا چون میخواهیم این کلاس محدود به یک مدل خاص نباشد، از یک اینترفیس برای شناسایی کلاسهای مجاز استفاده میکنیم .
public interface INeedMultiPartMediaTypeFormatter { }
و آنرا به کلاس newProduct اضافه میکنیم :
public class NewProduct : INeedMultiPartMediaTypeFormatter { ... }
public override bool CanReadType(Type type) { return typeof(INeedMultiPartMediaTypeFormatter).IsAssignableFrom(type); } public override bool CanWriteType(Type type) { return false; }
و اما تابع ReadFromStreamAsync که کار خواندن محتوای ارسال شده و بایند کردن آنها به پارامترها را برعهده دارد
public async override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger)
ابتدا محتوای ارسال شده را خوانده و اطلاعات فرم را استخراج میکنیم و از طرف دیگر با استفاده از کلاس Activator یک نمونه از مدل جاری را ساخته و لیست propertyهای آنرا استخراج میکنیم.
MultipartMemoryStreamProvider provider = await content.ReadAsMultipartAsync(); IEnumerable<HttpContent> formData = provider.Contents.AsEnumerable(); var modelInstance = Activator.CreateInstance(type); IEnumerable<PropertyInfo> properties = type.GetProperties();
سپس در یک حلقه به ترتیب برای هر property متعلق به مدل، در میان اطلاعات فرم جستجو میکنیم. برای پیدا کردن اطلاعات متناظر با هر property در هدر Content-Disposition که در بالا توضیح داده شد، به دنبال فیلد همنام با property میگردیم .
foreach (PropertyInfo prop in properties) { var propName = prop.Name.ToLower(); var propType = prop.PropertyType; var data = formData.FirstOrDefault(d => d.Headers.ContentDisposition.Name.ToLower().Contains(propName));
گفتیم که هر فیلد یک هدر، Content-Type هم میتواند داشته باشد. این هدر به صورت پیش فرض معادل text/plain است و برای فیلدهای عادی قرار داده نمیشود . در این مثال چون فقط یک
فیلد غیر رشته ای داریم فرض را بر این گرفته ایم که در صورت وجود Content-Type، فیلد مربوط به تصویر است. در صورتیکهContentType وجود داشته باشد، محتوای فیلد را به شکل Stream
خوانده به byte[] تبدیل و با استفاده از متد SetValue در property مربوطه قرار میدهیم.
if (data != null) { if (data.Headers.ContentType != null) { using (var fileStream = await data.ReadAsStreamAsync()) { using (MemoryStream ms = new MemoryStream()) { fileStream.CopyTo(ms); prop.SetValue(modelInstance, ms.ToArray()); } } }
در صورتی که Content-Type غایب باشد بدین معنی است که محتوای فیلد از نوع رشته است ( عدد ، تاریخ ، guid ، رشته ) و باید به نوع مناسب تبدیل شود. ابتدا آن را به صورت یک رشته میخوانیم و با استفاده از Convert.ChangeType آنرا به نوع مناسب تبدیل میکنیم و در property متناظر قرار میدهیم .
if (data != null) { if (data.Headers.ContentType != null) { //... } else { string rawVal = await data.ReadAsStringAsync(); object val = Convert.ChangeType(rawVal, propType); prop.SetValue(modelInstance, val); } }
return modelInstance;
config.Formatters.Add(new MultiPartMediaTypeFormatter());
ASP.NET MVC #12
+ و یا همچنین layout، مدل محتوای خودش را به ارث میبرد. یعنی مدلی که در View تنظیم میشود، همان مدلی است که layout به آن دسترسی خواهد داشت. به همین جهت مثلا میتونید توسط ViewBag، عنوان صفحه را که در layout تعریف شده، مقدار دهی کنید.
اگر میخواهید Strongly typed کار کنید، روش Html.RenderAction یک راه حل است و روش دوم به صورت زیر است:
یک کلاس پایه abstract تعریف کنید:
public abstract class BaseViewModel { public string Name { get; set; } }
public class HomeViewModel : BaseViewModel { public int Data1 { set; get;} // ... }
@model BaseViewModel <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Test</title> </head> <body> <header> Hello @Model.Name </header> <div> @this.RenderBody() </div> </body> </html>
bower یک فناوری منسوخ شدهاست و اگر بخواهیم bower.json ذکر شدهی در این مطلب را با package.json مربوط به npm جایگزین کنیم، به یک چنین محتوایی خواهیم رسید:
{ "name": "testwebapp", "version": "1.0.0", "description": "", "scripts": {}, "author": "", "license": "ISC", "dependencies": { "bootstrap": "^3.3.7", "jquery": "^2.2.4", "jquery-ajax-unobtrusive": "^3.2.4", "jquery-validation": "^1.17.0", "jquery-validation-unobtrusive": "^3.2.8" } }
سپس بر اساس مسیرهای پوشهی node_modules جدید، فایل bundleconfig.json چنین محتوایی را پیدا میکند:
[ { "outputFileName": "wwwroot/css/site.min.css", "inputFiles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "wwwroot/css/site.css" ] }, { "outputFileName": "wwwroot/js/site.min.js", "inputFiles": [ "node_modules/jquery/dist/jquery.min.js", "node_modules/jquery-validation/dist/jquery.validate.min.js", "node_modules/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js", "node_modules/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js", "node_modules/bootstrap/dist/js/bootstrap.min.js", "wwwroot/js/site.js" ], "minify": { "enabled": true, "renameLocals": true }, "sourceMap": false } ]
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - DNTCommon.Web.Core.TestWebApp</title> <link href="~/css/site.min.css" rel="stylesheet" asp-append-version="true" /> </head> <body> <div> @RenderBody() </div> <script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script> @RenderSection("Scripts", required: false) </body> </html>
<Project Sdk="Microsoft.NET.Sdk.Web"> <Target Name="PrecompileScript" BeforeTargets="BeforeBuild"> <Exec Command="dotnet bundle" /> </Target> <ItemGroup> <DotNetCliToolReference Include="BundlerMinifier.Core" Version="2.6.362" /> </ItemGroup> </Project>
برخلاف تصور عموم، ساختار یک صفحه PDF شبیه به یک صفحه فایل Word نیست. این صفحات درحقیقت نوعی Canvas برای نقاشی هستند. در این بوم نقاشی، شکل، تصویر، متن و غیره در مختصات خاصی قرار خواهند گرفت. حتی کلمه «متن» میتواند به صورت سه حرف در سه مختصات خاص یک صفحه PDF نقاشی شود. برای درک بهتر این مورد نیاز است سورس یک صفحه PDF را بررسی کرد.
نحوه استخراج سورس یک صفحه PDF
using System.Diagnostics; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; namespace TestReaders { class Program { static void writePdf() { using (var document = new Document(PageSize.A4)) { var writer = PdfWriter.GetInstance(document, new FileStream("test.pdf", FileMode.Create)); document.Open(); document.Add(new Paragraph("Test")); PdfContentByte cb = writer.DirectContent; BaseFont bf = BaseFont.CreateFont(); cb.BeginText(); cb.SetFontAndSize(bf, 12); cb.MoveText(88.66f, 367); cb.ShowText("ld"); cb.MoveText(-22f, 0); cb.ShowText("Wor"); cb.MoveText(-15.33f, 0); cb.ShowText("llo"); cb.MoveText(-15.33f, 0); cb.ShowText("He"); cb.EndText(); PdfTemplate tmp = cb.CreateTemplate(250, 25); tmp.BeginText(); tmp.SetFontAndSize(bf, 12); tmp.MoveText(0, 7); tmp.ShowText("Hello People"); tmp.EndText(); cb.AddTemplate(tmp, 36, 343); } Process.Start("test.pdf"); } private static void readPdf() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { byte[] contentBytes = reader.GetPageContent(i); File.WriteAllBytes("page-" + i + ".txt", contentBytes); } reader.Close(); } static void Main(string[] args) { writePdf(); readPdf(); } } }
اگر علاقمند باشید که سورس واقعی صفحات یک فایل PDF را مشاهده کنید، نحوه انجام آن توسط کتابخانه iTextSharp به صورت فوق است.
هرچند متد GetPageContent آرایهای از بایتها را بر میگرداند، اما اگر حاصل نهایی را در یک ادیتور متنی باز کنیم، قابل مطالعه و خواندن است. برای مثال، سورس مثال فوق (محتوای فایل page-1.txt تولید شده) به نحو زیر است:
q BT 36 806 Td 0 -18 Td /F1 12 Tf (Test)Tj 0 0 Td ET Q BT /F1 12 Tf 88.66 367 Td (ld)Tj -22 0 Td (Wor)Tj -15.33 0 Td (llo)Tj -15.33 0 Td (He)Tj ET q 1 0 0 1 36 343 cm /Xf1 Do Q
SaveGraphicsState(); // q BeginText(); // BT MoveTextPos(36, 806); // Td MoveTextPos(0, -18); // Td SelectFontAndSize("/F1", 12); // Tf ShowText("(Test)"); // Tj MoveTextPos(0, 0); // Td EndTextObject(); // ET RestoreGraphicsState(); // Q BeginText(); // BT SelectFontAndSize("/F1", 12); // Tf MoveTextPos(88.66, 367); // Td ShowText("(ld)"); // Tj MoveTextPos(-22, 0); // Td ShowText("(Wor)"); // Tj MoveTextPos(-15.33, 0); // Td ShowText("(llo)"); // Tj MoveTextPos(-15.33, 0); // Td ShowText("(He)"); // Tj EndTextObject(); // ET SaveGraphicsState(); // q TransMatrix(1, 0, 0, 1, 36, 343); // cm XObject("/Xf1"); // Do RestoreGraphicsState(); // Q
تا اینجا استخراج متن از فایلهای PDF ساده به نظر میرسد. باید به دنبال Tj گشت و حروف مرتبط با آنرا ذخیره کرد. اما در مورد «ترسیم» عبارات hello world و hello people اینطور نیست. عبارت hello world به حروف متفاوتی تقسیم شده و سپس در مختصات مشخصی ترسیم میگردد. عبارت hello people به صورت یک شیء ذخیره شده در قسمت منابع فایل PDF، بازیابی و نمایش داده میشود و اصلا در سورس صفحه جاری وجود ندارد.
این تازه قسمتی از نحوه عملکرد فایلهای PDF است. در فایلهای PDF میتوان قلمها را مدفون ساخت. همچنین این قلمها نیز تنها زیر مجموعهای از قلم اصلی مورد استفاده هستند. برای مثال اگر عبارت Test قرار است نمایش داده شود، فقط اطلاعات T، e و s در فایل نهایی PDF قرار میگیرند. به علاوه امکان تغییر کلی شماره Glyph متناظر با هر حرف نیز توسط PDF writer وجود دارد. به عبارتی الزامی نیست که مشخصات اصلی فونت حتما حفظ شود.
شاید بعضی از PDFهای فارسی را دیده باشید که پس از کپی متن آنها در برنامه Adobe reader و سپس paste آن در جایی دیگر، متن حاصل قابل خواندن نیست. علت این است که نحوه ذخیره سازی قلم مورد استفاده کاملا تغییر کرده است و برای بازیابی متن اینگونه فایلها، استفاده از OCR سادهترین روش است. برای نمونه در این قلم جدید مدفون شده، دیگر شماره کاراکتر 0x41 مساوی A نیست. بنابر سلیقه PDF writer این شماره به Glyph دیگری انتساب داده شده و چون قلم و مشخصات هندسی Glyph مورد استفاده در فایل PDF ذخیره میشود، برای نمایش این نوع فایلها هیچگونه مشکلی وجود ندارد. اما متن آنها به سادگی قابل بازیابی نیست.
Rss آب و هوای هر شهر در یاهو به صورت یک لینک یکتا میباشد؛ به شکل زیر :
و این لینک http://weather.yahooapis.com/forecastrss?w=28350859&u=c اطلاعات آب و هوای تهران را در قالب یک RSS به شما نمایش خواهد داد.
خوب، حالا پارامتر دوم یعنی پارامتر u چکاری را انجام میدهد؟
* چنانچه مقدار پارامتر u برابر c باشد، یعنی شما دمای آب و هوای شهر مد نظر را بر اساس سانتیگراد میخواهید.
* اگر مقدار پارامتر u برابر f باشد، یعنی شما دمای آب و هوای آن شهر مورد نظر را بر اساس فارنهایت میخواهید.
برای گرفتن WOEID شهرها هم به این سایت بروید http://woeid.rosselliot.co.nz و اسم هر شهری که میخواهید بزنید تا WOEID را به شما نمایش دهد.
در این مثال من از یک DropDown استفاده کردم که کاربر با انتخاب هر شهر از DropDown، آب و هوای آن شهر را مشاهده میکند.
Action مربوط به صفحهی Index به صورت زیر میباشد :
[HttpGet] public ActionResult Index() { ViewBag.ProvinceList = _RPosition.Positions; ShowWeatherProvince(8); return View(); }
حال تابعی را که آب و هوای مربوط به هر شهر را نمایش میدهد، به شرح زیر است:
public ActionResult ShowWeatherProvince(int dpProvince) { XDocument rssXml=null; CountryName CountryName = new CountryName(); if (dpProvince != 0) { switch (dpProvince) { case 1: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345768&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Azarbayejan-e Sharqhi" }; break; } case 2: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345767&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Azarbayejan-e Qarbi" }; break; } case 3: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254335&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Ardebil" }; break; } case 4: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=28350859&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Alborz" }; break; } case 5: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345787&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Esfahan" }; break; } case 6: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345775&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Ilam" }; break; } case 7: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254463&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Bushehr" }; break; } case 8: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=28350859&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Tehran" }; break; } case 9: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345769&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Chahar Mahall va Bakhtiari" }; break; } case 10: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=56189824&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Razavi Khorasan" }; break; } case 11: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345789&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Shomali Khorasan" }; break; } case 12: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345789&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Jonubi Khorasan" }; break; } case 13: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345778&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Khuzestan" }; break; } case 14: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2255311&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Zanjan" }; break; } case 15: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345784&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Semnan" }; break; } case 16: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345770&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Sistan va Baluchestan" }; break; } case 17: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345772&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Fars" }; break; } case 18: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=20070200&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Qazvin" }; break; } case 19: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2255062&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Qom" }; break; } case 20: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345779&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kordestan" }; break; } case 21: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254796&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kerman" }; break; } case 22: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254797&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kermanshah" }; break; } case 23: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345771&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kohgiluyeh va Buyer Ahmad" }; break; } case 24: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=20070201&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Golestan" }; break; } case 25: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345773&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Gilan" }; break; } case 26: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345782&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Lorestan" }; break; } case 27: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345783&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Markazi" }; break; } case 28: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345780&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Mazandaran" }; break; } case 29: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254664&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Hamedan" }; break; } case 30: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345776&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Hormozgan" }; break; } case 31: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2253355&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Yazd" }; break; } } ViewBag.Location = CountryName; XNamespace yWeatherNS = "http://xml.weather.yahoo.com/ns/rss/1.0"; List<YahooWeatherRssItem> WeatherList = new List<YahooWeatherRssItem>(); for (int i = 0; i < 4; i++) { YahooWeatherRssItem YahooWeatherRssItem = new YahooWeatherRssItem() { Code = Convert.ToInt32(rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("code").Value), Day = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("day").Value, Low = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("low").Value, High = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("high").Value, Text = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("text").Value, }; WeatherList.Add(YahooWeatherRssItem); } ViewBag.FeedList = WeatherList; } return PartialView("_Weather"); }
حالا کد مربوط به خواندن فایل Rss را برایتان توضیح میدهم : حلقهی for 0 تا 4 (که در کد بالا مشاهده میکنید)یعنی اطلاعات 4 روز آینده را برایم برگردان.
من تگهای Code ، Day ، Low ، High و text فایل RSS را در این حلقه For میخوانم که البته مقادیر این 4 روز را در لیستی اضافه میکنم که نوع این لیست هم از نوع YahooWeatherRssItem میباشد. من این کلاس را در فایل ضمیمه قرار دادم. اکنون هر کدام از این تگها را برایتان توضیح میدهم:
code : هر آب و هوا کدی دارد .مثلا آب و هوای نیمه ابری یک کد ، آب و هوای آفتابی کدی دیگر و ...
Low: حداقل دمای آن روز را به ما میدهد .
High: حداکثر دمای آن روز را به میدهد .
day: نام روز از هفته را بر میگرداند مثلا شنبه ، یکشنبه و ....
text: که توضیحاتی میدهد مثلا اگر هوا آفتابی باشد مقدار sunny را بر میگرداند و ...
خوب، تا اینجا ما Rss مربوط به هر شهر را خواندیم حالا در قسمت Design باید چکار کنیم .
کدهای html صفحهی Index ما شامل کدهای زیر است :
@{ ViewBag.Title = "Weather"; } <link href="~/Content/User/Weather/Weather.css" rel="stylesheet" /> @section scripts{ <script src="@Url.Content("~/Scripts/jquery-1.6.2.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script> <script type="text/javascript"> $("#dpProvince").change(function () { $(this).parents("form").submit(); }); </script> } <h2>Weather</h2> <div id="Progress"> <img src="~/Images/User/Other/ajax-loader.gif" /> </div> <div id="BoxContent"> @Html.Partial("_Weather")</div> @using (Ajax.BeginForm(actionName: "ShowWeatherProvince", ajaxOptions: new AjaxOptions { UpdateTargetId = "BoxContent", LoadingElementId = "Progress", InsertionMode = InsertionMode.Replace })) { <div style="padding-top:15px;"> <div style="float:left; width:133px; ">Select Your Province</div> <div style="float:left"> @Html.DropDownList("dpProvince", new SelectList(ViewBag.ProvinceList, "Id", "Name"),"Select Your Province", new { @class = "webUserDropDown", @style = "width:172px" })</div> </div> }
@{ List<Weather.YahooWeatherRssItem> Feeds = ViewBag.FeedList; } <div> @{ HtmlString StartTable = new HtmlString("<table class='WeatherTable' cellspacing='0' cellpadding='0'><tbody><tr>"); HtmlString EndTable = new HtmlString("</tr></tbody></table>"); HtmlString StartTD = new HtmlString("<td>"); HtmlString EndTD = new HtmlString("</td>"); } <div style="width: 300px;"> @{ @StartTable foreach (var item in Feeds) { @StartTD <div>@item.Day</div> <div> @{ string FileName = ""; switch (item.Code) { case 0: { FileName = "/Images/User/Weather/Tornado.png"; break; } case 1: { FileName = "/Images/User/Weather/storm2.gif"; break; } case 2: { FileName = "/Images/User/Weather/storm2.gif"; break; } case 3: { FileName = "/Images/User/Weather/storm2.gif"; break; } case 4: { FileName = "/Images/User/Weather/15.gif"; break; } case 5: { FileName = "/Images/User/Weather/29.gif"; break; } case 6: { FileName = "/Images/User/Weather/29.gif"; break; } case 7: { FileName = "/Images/User/Weather/29.gif"; break; } case 8: { FileName = "/Images/User/Weather/26.gif"; break; } case 9: { FileName = "/Images/User/Weather/drizzle.png"; break; } case 10: { FileName = "/Images/User/Weather/26.gif"; break; } case 11: { FileName = "/Images/User/Weather/18.gif"; break; } case 12: { FileName = "/Images/User/Weather/18.gif"; break; } case 13: { FileName = "/Images/User/Weather/19.gif"; break; } case 14: { FileName = "/Images/User/Weather/19.gif"; break; } case 15: { FileName = "/Images/User/Weather/19.gif"; break; } case 16: { FileName = "/Images/User/Weather/22.gif"; break; } case 17: { FileName = "/Images/User/Weather/Hail.png"; break; } case 18: { FileName = "/Images/User/Weather/25.gif"; break; } case 19: { FileName = "/Images/User/Weather/dust.png"; break; } case 20: { FileName = "/Images/User/Weather/fog_icon.png"; break; } case 21: { FileName = "/Images/User/Weather/hazy_icon.png"; break; } case 22: { FileName = "/Images/User/Weather/2017737395.png"; break; } case 23: { FileName = "/Images/User/Weather/32.gif"; break; } case 24: { FileName = "/Images/User/Weather/32.gif"; break; } case 25: { FileName = "/Images/User/Weather/31.gif"; break; } case 26: { FileName = "/Images/User/Weather/7.gif"; break; } case 27: { FileName = "/Images/User/Weather/38.gif"; break; } case 28: { FileName = "/Images/User/Weather/6.gif"; break; } case 29: { FileName = "/Images/User/Weather/35.gif"; break; } case 30: { FileName = "/Images/User/Weather/7.gif"; break; } case 31: { FileName = "/Images/User/Weather/33.gif"; break; } case 32: { FileName = "/Images/User/Weather/1.gif"; break; } case 33: { FileName = "/Images/User/Weather/34.gif"; break; } case 34: { FileName = "/Images/User/Weather/2.gif"; break; } case 35: { FileName = "/Images/User/Weather/freezing_rain.png"; break; } case 36: { FileName = "/Images/User/Weather/30.gif"; break; } case 37: { FileName = "/Images/User/Weather/15.gif"; break; } case 38: { FileName = "/Images/User/Weather/15.gif"; break; } case 39: { FileName = "/Images/User/Weather/15.gif"; break; } case 40: { FileName = "/Images/User/Weather/12.gif"; break; } case 41: { FileName = "/Images/User/Weather/22.gif"; break; } case 42: { FileName = "/Images/User/Weather/22.gif"; break; } case 43: { FileName = "/Images/User/Weather/22.gif"; break; } case 44: { FileName = "/Images/User/Weather/39.gif"; break; } case 45: { FileName = "/Images/User/Weather/thundershowers.png"; break; } case 46: { FileName = "/Images/User/Weather/19.gif"; break; } case 47: { FileName = "/Images/User/Weather/thundershowers.png"; break; } case 3200: { FileName = "/Images/User/Weather/1211810662.png"; break; } } } <img alt='@item.Text' title='@item.Text' src='@FileName'> </div> <div> <span>@item.High°</span> <span>@item.Low°</span> </div> @EndTD } } @EndTable </div> </div>
چنانچه در مورد RSS وضعیت آب و هوای یاهو اطلاعات دقیقتری را میخواهید بدانید به این لینک بروید.
در آموزش بعدی قصد دارم برایتان این بخش را توضیح دهم که بر اساس IP بازدید کننده سایت شما، اطلاعات آب و هوایی شهر بازدید کننده را برایش در سایت نمایش دهد.
Files-06bf65bac63d4dd694b15fc24d4cb074.zip
موفق باشید