در این میان یکی از دغدغههای توسعه دهندگان وب ، آپلود فایلها به صورت آنی (مثل attach files گوگل) میباشد. برای حل این مسئله ، ابزارها و پلاگینهای متعددی وجود دارد که در اینجا به 10 تا از پلاگینهای Jquery اشاره شده است.
به شخصه با پلاگین Uploadify کار کرده ام و از استفاده از آن راضی هستم ولی همین دیشب برای قسمتی از یک پروژه نیاز
به ابزاری جهت آپلود فایلها با امکانات مورد نظرم داشتم که به PlUpload برخورد کردم.
از امکاناتی که این ابزار در اختیار شما قرار میدهد :
- یک اینترفیس زیبا جهت آپلود و افزودن فایل ها
- پشتیبانی از زبانهای مختلف و همین طور زبان فارسی
- امکان استفاده از قالب Jquery UI
- Drag&Drop برای مرورگرهایی که از Html5 پشتیبانی میکنند
حال که با امکانات این ابزار بیشتر آشنا شدید بریم سراغ استفاده از این ابزار در asp.net mvc :)
ابتدا پروژه را از اینجا دانلود کنید. سپس یک پروژهی جدید mvc 3 بسازید (از نوع Internet Application و با نام دلخواه). سپس پوشهی plupload را در قسمت سلوشن برنامه کپی کنید.
حال در فایل Views->Shared->_Layout.cshtml ، تگ head را جهت افزودن امکانات پلاگین این گونه تغییر دهید :
<title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" /> <link href="../../plupload/js/jquery.plupload.queue/css/jquery.plupload.queue.css" rel="stylesheet" /> <script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" type="text/javascript"></script> <script type="text/javascript" src="http://bp.yahooapis.com/2.4.21/browserplus-min.js"></script> <script src="../../plupload/js/plupload.full.js"></script> <script src="../../plupload/js/jquery.plupload.queue/jquery.plupload.queue.js"></script> <script src="../../plupload/js/i18n/fa.js"></script>
نکته : فایل fa.js که جهت استفاده از زبان فارسی در اینترفیس آپلود فایلها میباشد، که وجود آن در آدرس واضح میباشد.
سپس به فایل Views->Home->Index.cshtml بروید و آن را این گونه دوباره نویسی کنید :
@{ ViewBag.Title = "Uploading Files using PlUpload"; } <h2>@ViewBag.Message</h2> @using (Html.BeginForm("Post", "home", FormMethod.Post, new { enctype = "multipart/form-data" })) { <div id="uploader"> <p>You browser doesn't have Flash, Silverlight, Gears, BrowserPlus or HTML5 support.</p> </div> } <script> $(function () { $("#uploader").pluploadQueue({ // General settings runtimes: 'html5,gears,flash,silverlight,browserplus,html4', url: '@Url.Action("Upload" , "Home")', max_file_size: '10mb', chunk_size: '1mb', unique_names: true, // Resize images on clientside if we can resize: { width: 320, height: 240, quality: 90 }, // Specify what files to browse for filters: [ { title: "Image files", extensions: "jpg,gif,png" }, { title: "Zip files", extensions: "zip" } ], // Flash settings flash_swf_url: '/plupload/js/plupload.flash.swf', // Silverlight settings silverlight_xap_url: '/plupload/js/plupload.silverlight.xap' }); }); </script>
- جهت آپلود فایلها تگ enctype = "multipart/form-data" را فراموش نکنید.
- در قسمت مقداردهی به ویژگیهای Plupload ، قسمت runtime به صورت ترتیبی کار میکند لذا اگر اولی پشتیبانی نشود سراغ دومی میرود و اگر دومی نشود سومی و ... در صفحهی اول سایت PlUpload ، موارد پشتیبانی شده توسط تکنولوژیها آورده شده است لذا این ترتیب را ترتیب مناسبی میبینم و اگر اولین مورد html5 باشد امکان Drag&Drop وجود خواهد داشت.
خود سایت PlUpload داکیومنت خیلی خوبی جهت توضیح موارد مختلف دارد لذا توضیح دوباره لازم نیست.
همان طور که در ویژگی url مشاهده میکنید به کنترلر Home و اکشن متود Upload اشاره شده است که طرز کار به این گونه است که هر بار که یک فایل آپلود میشود درخواستی به این آدرس و محتوای فایل در قسمت Request.Files ارسال میشود و همین طور نام فایل که unique ارسال میشود و chunk که تیکههای فایل است(پست میشود).
پس اکشنی با نام Upload در کنترلر HomeController بسازید :
[HttpPost] public ActionResult Upload(int? chunk, string name) { var fileUpload = Request.Files[0]; var uploadPath = Server.MapPath("~/App_Data"); chunk = chunk ?? 0; using (var fs = new FileStream(Path.Combine(uploadPath, name), chunk == 0 ? FileMode.Create : FileMode.Append)) { if (fileUpload != null) { var buffer = new byte[fileUpload.InputStream.Length]; fileUpload.InputStream.Read(buffer, 0, buffer.Length); fs.Write(buffer, 0, buffer.Length); } } return Content("chunk uploaded", "text/plain"); }
حال برنامه را اجرا کنید و از این ابزار لذت ببرید:)
نکاتی که باید جهت بررسی یکسان بودن دو URL بررسی کرد
using System; using System.Web; namespace UrlNormalizationTest { public static class UrlNormalization { public static bool AreTheSameUrls(this string url1, string url2) { url1 = url1.NormalizeUrl(); url2 = url2.NormalizeUrl(); return url1.Equals(url2); } public static bool AreTheSameUrls(this Uri uri1, Uri uri2) { var url1 = uri1.NormalizeUrl(); var url2 = uri2.NormalizeUrl(); return url1.Equals(url2); } public static string[] DefaultDirectoryIndexes = new[] { "default.asp", "default.aspx", "index.htm", "index.html", "index.php" }; public static string NormalizeUrl(this Uri uri) { var url = urlToLower(uri); url = limitProtocols(url); url = removeDefaultDirectoryIndexes(url); url = removeTheFragment(url); url = removeDuplicateSlashes(url); url = addWww(url); url = removeFeedburnerPart(url); return removeTrailingSlashAndEmptyQuery(url); } public static string NormalizeUrl(this string url) { return NormalizeUrl(new Uri(url)); } private static string removeFeedburnerPart(string url) { var idx = url.IndexOf("utm_source=", StringComparison.Ordinal); return idx == -1 ? url : url.Substring(0, idx - 1); } private static string addWww(string url) { if (new Uri(url).Host.Split('.').Length == 2 && !url.Contains("://www.")) { return url.Replace("://", "://www."); } return url; } private static string removeDuplicateSlashes(string url) { var path = new Uri(url).AbsolutePath; return path.Contains("//") ? url.Replace(path, path.Replace("//", "/")) : url; } private static string limitProtocols(string url) { return new Uri(url).Scheme == "https" ? url.Replace("https://", "http://") : url; } private static string removeTheFragment(string url) { var fragment = new Uri(url).Fragment; return string.IsNullOrWhiteSpace(fragment) ? url : url.Replace(fragment, string.Empty); } private static string urlToLower(Uri uri) { return HttpUtility.UrlDecode(uri.AbsoluteUri.ToLowerInvariant()); } private static string removeTrailingSlashAndEmptyQuery(string url) { return url .TrimEnd(new[] { '?' }) .TrimEnd(new[] { '/' }); } private static string removeDefaultDirectoryIndexes(string url) { foreach (var index in DefaultDirectoryIndexes) { if (url.EndsWith(index)) { url = url.TrimEnd(index.ToCharArray()); break; } } return url; } } }
using NUnit.Framework; using UrlNormalizationTest; namespace UrlNormalization.Tests { [TestFixture] public class UnitTests { [Test] public void Test1ConvertingTheSchemeAndHostToLowercase() { var url1 = "HTTP://www.Example.com/".NormalizeUrl(); var url2 = "http://www.example.com/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test2CapitalizingLettersInEscapeSequences() { var url1 = "http://www.example.com/a%c2%b1b".NormalizeUrl(); var url2 = "http://www.example.com/a%C2%B1b".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test3DecodingPercentEncodedOctetsOfUnreservedCharacters() { var url1 = "http://www.example.com/%7Eusername/".NormalizeUrl(); var url2 = "http://www.example.com/~username/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test4RemovingTheDefaultPort() { var url1 = "http://www.example.com:80/bar.html".NormalizeUrl(); var url2 = "http://www.example.com/bar.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test5AddingTrailing() { var url1 = "http://www.example.com/alice".NormalizeUrl(); var url2 = "http://www.example.com/alice/?".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test6RemovingDotSegments() { var url1 = "http://www.example.com/../a/b/../c/./d.html".NormalizeUrl(); var url2 = "http://www.example.com/a/c/d.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test7RemovingDirectoryIndex1() { var url1 = "http://www.example.com/default.asp".NormalizeUrl(); var url2 = "http://www.example.com/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test7RemovingDirectoryIndex2() { var url1 = "http://www.example.com/default.asp?id=1".NormalizeUrl(); var url2 = "http://www.example.com/default.asp?id=1".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test7RemovingDirectoryIndex3() { var url1 = "http://www.example.com/a/index.html".NormalizeUrl(); var url2 = "http://www.example.com/a/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test8RemovingTheFragment() { var url1 = "http://www.example.com/bar.html#section1".NormalizeUrl(); var url2 = "http://www.example.com/bar.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test9LimitingProtocols() { var url1 = "https://www.example.com/".NormalizeUrl(); var url2 = "http://www.example.com/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test10RemovingDuplicateSlashes() { var url1 = "http://www.example.com/foo//bar.html".NormalizeUrl(); var url2 = "http://www.example.com/foo/bar.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test11AddWww() { var url1 = "http://example.com/".NormalizeUrl(); var url2 = "http://www.example.com".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test12RemoveFeedburnerPart() { var url1 = "http://site.net/2013/02/firefox-19-released/?utm_source=rss&utm_medium=rss&utm_campaign=firefox-19-released".NormalizeUrl(); var url2 = "http://site.net/2013/02/firefox-19-released".NormalizeUrl(); Assert.AreEqual(url1, url2); } } }
برپایی پیشنیازها
در اینجا برای بررسی مسیریابی، یک پروژهی جدید React را ایجاد میکنیم.
> create-react-app sample-15 > cd sample-15 > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
همچنین کتابخانهی ثالث بسیار معروف react-router-dom را نیز نصب میکنیم:
> npm i react-router-dom --save
افزودن مسیریابی به برنامه
پس از نصب کتابخانهی react-router-dom، برای افزودن آن به برنامه و فعالسازی مسیریابی، به فایل index.js مراجعه کرده و import آنرا به ابتدای فایل اضافه میکنیم:
import { BrowserRouter } from "react-router-dom";
ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById("root") );
ثبت و معرفی مسیریابیها
در ادامه باید مسیریابیهای خود را ثبت کنیم؛ به این معنا که بر اساس URL خاصی، چه کامپوننتی باید رندر شود. به همین جهت پوشهی جدید src\components را ایجاد کرده و کامپوننت src\components\navbar.jsx را که یک کامپوننت تابعی بدون حالت است، در آن تعریف میکنیم:
import React from "react"; const NavBar = () => { return ( <nav className="navbar bg-dark navbar-dark navbar-expand-sm"> <div className="navbar-nav"> <a className="nav-item nav-link" href="/"> Home </a> <a className="nav-item nav-link" href="/products"> Products </a> <a className="nav-item nav-link" href="/posts/2018/06"> Posts </a> <a className="nav-item nav-link" href="/admin"> Admin </a> </div> </nav> ); }; export default NavBar;
سپس به فایل app.js مراجعه کرده و ساختار آنرا به صورت زیر، جهت درج این NavBar، ویرایش میکنیم تا سبب رندر و نمایش منوی راهبری در مرورگر شود:
import "./App.css"; import React, { Component } from "react"; import NavBar from "./components/navbar"; class App extends Component { render() { return ( <div> <NavBar /> </div> ); } } export default App;
import "./App.css"; import React, { Component } from "react"; import { Route } from "react-router-dom"; import Dashboard from "./components/admin/dashboard"; import Home from "./components/home"; import NavBar from "./components/navbar"; import Posts from "./components/posts"; import Products from "./components/products"; class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </div> </div> ); } } export default App;
کامپوننت جدید src\components\products.jsx جهت رندر لیست آرایهی اشیاء product:
import React, { Component } from "react"; class Products extends Component { state = { products: [ { id: 1, name: "Product 1" }, { id: 2, name: "Product 2" }, { id: 3, name: "Product 3" } ] }; render() { return ( <div> <h1>Products</h1> <ul> {this.state.products.map(product => ( <li key={product.id}> <a href={`/products/${product.id}`}>{product.name}</a> </li> ))} </ul> </div> ); } } export default Products;
کامپوننت بدون حالت تابعی src\components\home.jsx با این محتوا:
import React from "react"; const Home = () => { return <h1>Home</h1>; }; export default Home;
کامپوننت بدون حالت تابعی src\components\posts.jsx با این محتوا:
import React from "react"; const Posts = () => { return ( <div> <h1>Posts</h1> Year: , Month: </div> ); }; export default Posts;
کامپوننت بدون حالت تابعی src\components\admin\dashboard.jsx در پوشهی جدید admin با این محتوا:
import React from "react"; const Dashboard = ({ match }) => { return ( <div> <h1>Admin Dashboard</h1> </div> ); }; export default Dashboard;
معرفی کامپوننت Switch
<div className="container"> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </div>
یک روش حل این مشکل، استفاده از ویژگی exact است:
<Route path="/" exact component={Home} />
راه دوم رفع این مشکل، استفاده از کامپوننت Switch است. به همین جهت ابتدا این کامپوننت را import میکنیم:
import { Route, Switch } from "react-router-dom";
class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Switch> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </Switch> </div> </div> ); } }
بنابراین هنگام کار با Switch، ترتیب مسیریابیهای تعریف شده مهم است و باید از یک مسیریابی ویژه شروع شده و به یک مسیریابی عمومی مانند / ختم شود.
معرفی کامپوننت Link
تا اینجا اگر برنامه را اجرا کرده باشید و پیشتر سابقهی کار با برنامههای SPA یا Single page applications را داشته باشید، یک مشکل دیگر را نیز احساس کردهاید: سیستم مسیریابی که تا کنون تعریف کردهایم، به صورت SPA عمل نمیکند. یعنی به ازای هربار کلیک بر روی لینکهای منوی راهبری سایت، یکبار دیگر به طور کامل برنامه از صفر بارگذاری مجدد میشود و تمام اسکریپتهای آن مجددا از سرور دریافت شده و رندر خواهند شد. این مورد را در برگهی network ابزارهای توسعه دهندگان مرورگر خود بهتر میتوانید مشاهده کنید. به ازای هر درخواست نمایش کامپوننتی، تعدادی درخواست HTTP به سمت سرور ارسال میشوند که برای دریافت صفحهی index و bundle.js برنامه هستند. اما در برنامههای SPA، مانند جمیل، با هربار کلیک بر روی لینکی، شاهد ریفرش و بارگذاری مجدد کل آن صفحه نیستیم و تنها اطلاعات موجود در قسمت container به روز میشوند.
یک نکته: در اینجا ممکن است دو درخواست websocket و info را نیز مشاهده کنید. این دو مرتبط به hot module reloading هستند که با ذخیرهی برنامه در ادیتور VSCode، بلافاصله سبب به روز رسانی و ریفرش برنامه در مرورگر میشوند.
برای رفع مشکل SPA نبودن برنامه، باید به کامپوننت NavBar مراجعه کرده و تمام anchorهای استاندارد تعریف شدهی در آنرا با کامپوننت Link جایگزین کنیم:
import React from "react"; import { Link } from "react-router-dom"; const NavBar = () => { return ( <nav className="navbar bg-dark navbar-dark navbar-expand-sm"> <div className="navbar-nav"> <Link className="nav-item nav-link" to="/"> Home </Link> <Link className="nav-item nav-link" to="/products"> Products </Link> <Link className="nav-item nav-link" to="/posts/2018/06"> Posts </Link> <Link className="nav-item nav-link" to="/admin"> Admin </Link> </div> </nav> ); }; export default NavBar;
با این تغییرات اگر برنامه را اجرا کنیم، اینبار با کلیک بر روی هر لینک، دیگر شاهد بارگذاری کامل صفحه در مرورگر نخواهیم بود؛ بلکه تنها قسمت container ای که کامپوننت Route مسیریابی در آن درج شدهاست، به روز رسانی میشود و این عملیات نیز بسیار سریع است؛ از این جهت که محتوای این کامپوننتها از همان bundle.js حاوی تمام کدهای برنامه تامین میشود و این فایل تنها یکبار در آغاز برنامه از سرور خوانده شده و سپس توسط مرورگر پردازش میشود. بنابراین در برنامههای SPA، برخلاف برنامههای وب معمولی، هربار که کاربر آدرس متفاوتی را انتخاب میکند، بارگذاری مجدد برنامه و خوانده شدن محتوای متناظر از سرور صورت نمیگیرد؛ این محتوا هم اکنون در bundle.js برنامه مهیا است و قابلیت استفادهی آنی را دارد.
اما کامپوننت Link چگونه کار میکند؟
کامپوننت لینک در نهایت همان anchorهای استاندارد را رندر میکند؛ اما به هر کدام یک onClick را نیز اضافه میکند که سبب جلوگیری از رفتار پیشفرض anchor میشود. به همین جهت مرورگر درخواست اضافهای را به سمت سرور ارسال نمیکند. در اینجا مدیریت کنندهی onClick، تنها Url بالای صفحه را در مرورگر تغییر میدهد. اکنون که Url تغییر کردهاست، یکی از مسیریابیهای تعریف شده، با این Url تطابق یافته و سپس کامپوننت متناظر با آنرا رندر میکند.
بررسی Route props
اگر بر روی لینک نمایش products در منوی راهبری سایت کلیک کرده و سپس به خروجی افزونهی react developer tools دقت کنیم (تصویر فوق)، مشاهده میکنیم که این کامپوننت هم اکنون تعدادی خاصیت را به صورت props در اختیار دارد؛ مانند history (امکان هدایت کاربر را به صفحهای دیگر دارد)، location (آدرس جاری برنامه) و match (اطلاعاتی در مورد الگوریتم تطابق مسیر). کار تنظیم این props، توسط کامپوننت Route ای که کار ثبت مسیریابیها را انجام میدهد، صورت میگیرد. به عبارتی کامپوننت Route، محصور کنندهی کامپوننتی است که آنرا به عنوان پارامتر، دریافت و در صورت تطابق با مسیر جاری، آنرا رندر میکند. همچنین در این بین کار تزریق خواص props یاد شده را نیز انجام میدهد.
ارسال props سفارشی در حین مسیریابی به کامپوننتها
همانطور که بررسی کردیم، کامپوننت Route، حداقل سه خاصیت props را به کامپوننتهایی که رندر میکند، تزریق خواهد کرد. اما در اینجا برای تزریق خواص سفارشی چگونه باید عمل کرد؟
در حین کار با کامپوننت Route، برای ارسال props اضافی، بجای استفاده از ویژگی component آن، باید از ویژگی render استفاده کرد:
<Route path="/products" render={() => <Products param1="123" param2="456" />} />
البته اگر به تصویر فوق دقت کنید، سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، دیگر در اینجا حضور ندارند. برای رفع این مشکل باید تعریف arrow function انجام شده را به صورت زیر تغییر داد:
<Route path="/products" render={props => ( <Products param1="123" param2="456" {...props} /> )} />
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-15-part-01.zip
ASP.NET MVC #14
آشنایی با نحوه معرفی تعاریف طرحبندی سایت به کمک Razor
ممکن است یک سری از اصطلاحات را در قسمتهای قبل مانند master page در لابلای توضیحات ارائه شده، مشاهده کرده باشید. این نوع مفاهیم برای برنامه نویسهای ASP.NET Web forms آشنا است (و اگر با Web forms view engine در ASP.NET MVC کار کنید، دقیقا یکی است؛ البته با این تفاوت که فایل code behind آنها حذف شده است). به همین جهت در این قسمت برای تکمیل بحث، مروری خواهیم داشت بر نحوهی معرفی جدید آنها توسط Razor.
در یک پروژه جدید ASP.NET MVC و در پوشه Views\Shared\_Layout.cshtml آن، فایل Layout آن، مفهوم master page را دارد. در این نوع فایلها، زیر ساخت مشترک تمام صفحات سایت قرار میگیرند:
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>
<body>
@RenderBody()
</body>
</html>
اگر دقت کرده باشید، در هیچکدام از فایلهای Viewایی که تا این قسمت به پروژههای مختلف اضافه کردیم، تگهایی مانند body، title و امثال آن وجود نداشتند. در ASP.NET مرسوم است کلیه اطلاعات تکراری صفحات مختلف سایت را مانند تگهای یاد شده به همراه منویی که باید در تمام صفحات قرار گیرد یا footer مشترک بین تمام صفحات سایت، به یک فایل اصلی به نام master page که در اینجا layout نام گرفته، Refactor کنند. به این ترتیب حجم کدها و markup تکراری که باید در تمام Viewهای سایت قرار گیرند به حداقل خواهد رسید.
برای مثال محل قرار گیری تعاریف Content-Type تمام صفحات و همچنین favicon سایت، بهتر است در فایل layout باشد و نه در تک تک Viewهای تعریف شده:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="shortcut icon" href="@Url.Content("~/favicon.ico")" type="image/x-icon" />
در کدهای فوق یک نمونه پیش فرض فایل layout را مشاهده میکنید. در اینجا توسط متد RenderBody، محتوای رندر شده یک View درخواستی، به داخل تگ body تزریق خواهد شد.
تا اینجا در تمام مثالهای قبلی این سری، فایل layout در Viewهای اضافه شده معرفی نشد. اما اگر برنامه را اجرا کنیم باز هم به نظر میرسد که فایل layout اعمال شده است. علت این است که در صورت عدم تعریف صریح layout در یک View، این تعریف از فایل Views\_ViewStart.cshtml دریافت میگردد:
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
فایل ViewStart، محل تعریف کدهای تکراری است که باید پیش از اجرای هر View مقدار دهی یا اجرا شوند. برای مثال در اینجا میشود بر اساس نوع مرورگر، layout خاصی را به تمام Viewها اعمال کرد. مثلا یک layout ویژه برای مرورگرهای موبایلها و layout ایی دیگر برای مرورگرهای معمولی. امکان دسترسی به متغیرهای تعریف شده در یک View در فایل ViewStart از طریق ViewContext.ViewData میسر است.
ضمن اینکه باید درنظر داشت که میتوان فایل ViewStart را در زیر پوشههای پوشه اصلی View نیز قرار داد. مثلا اگر فایل ViewStart ایی در پوشه Views/Home قرار گرفت، این فایل محتوای ViewStart اصلی قرار گرفته در ریشه پوشه Views را بازنویسی خواهد کرد.
برای معرفی صریح فایل layout، تنها کافی است مسیر کامل فایل layout را در یک View مشخص کنیم:
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
اهمیت این مساله هم در اینجا است که یک سایت میتواند چندین layout یا master page داشته باشد. برای نمونه یک layout برای صفحات ورود اطلاعات؛ یک layout خاص هم مثلا برای صفحات گزارش گیری نهایی سایت.
همانطور که پیشتر نیز ذکر شد، در ASP.NET حرف ~ به معنای ریشه سایت است که در اینجا ابتدای محل جستجوی فایل layout را مشخص میکند.
به این ترتیب زمانیکه یک کنترلر، View خاصی را فراخوانی میکند، کار از فایل Views\Shared\_Layout.cshtml شروع خواهد شد. سپس View درخواستی پردازش شده و محتوای نهایی آن، جایی که متد RenderBody قرار دارد، تزریق خواهد شد.
همچنین مقدار ViewBag.Title ایی که در فایل View تعریف شده، در فایل layout جهت رندر مقدار تگ title استفاده میشود (انتقال یک متغیر از View به یک فایل master page؛ کلاس layout، مدل View ایی را که قرار است رندر کند به ارث میبرد).
یک نکته:
در نگارش سوم ASP.NET MVC امکان بکارگیری حرف ~ به صورت مستقیم در حین تعریف یک فایل js یا css وجود ندارد و حتما باید از متد سمت سرور Url.Content کمک گرفت. در نگارش چهارم ASP.NET MVC، این محدودیت برطرف شده و دقیقا همانند متغیر Layout ایی که در بالا مشاهده میکنید، میتوان بدون نیاز به متد Url.Content، مستقیما از حرف ~ کمک گرفت و به صورت خودکار پردازش خواهد شد.
تزریق نواحی ویژه یک View در فایل layout
توسط متد RenderBody، کل محتوای View درخواستی در موقعیت تعریف شده آن در فایل Layout، رندر میشود. این ویژگی به نحو یکسانی به تمام Viewها اعمال میشود. اما اگر نیاز باشد تا view بتواند محتوای markup قسمت ویژهای از master page را مقدار دهی کند، میتوان از مفهومی به نام Sections استفاده کرد:
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>
<body>
<div id="menu">
@if (IsSectionDefined("Menu"))
{
RenderSection("Menu", required: false);
}
else
{
<span>This is the default ...!</span>
}
</div>
<div id="body">
@RenderBody()
</div>
</body>
</html>
در اینجا ابتدا بررسی میشود که آیا قسمتی به نام Menu در View جاری که باید رندر شود وجود دارد یا خیر. اگر بله، توسط متد RenderSection، آن قسمت نمایش داده خواهد شد. در غیراینصورت، محتوای پیش فرضی را در صفحه قرار میدهد. البته اگر از متد RenderSection با آرگومان required: false استفاده شود، درصورتیکه View جاری حاوی قسمتی به نام مثلا menu نباشد، تنها چیزی نمایش داده نخواهد شد. اگر این آرگومان را حذف کنیم، یک استثنای عدم یافت شدن ناحیه یا قسمت مورد نظر صادر میگردد.
نحوهی تعریف یک Section در Viewهای برنامه به شکل زیر است:
@{
ViewBag.Title = "Index";
//Layout = null;
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>
Index</h2>
@section Menu{
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
}
برای مثال فرض کنید که یکی از Viewهای شما نیاز به دو فایل اضافی جاوا اسکریپت برای اجرای صحیح خود دارد. میتوان تعاریف الحاق این دو فایل را در قسمت header فایل layout قرار داد تا در تمام Viewها به صورت خودکار لحاظ شوند. یا اینکه یک section سفارشی را به نحو زیر در آن View خاص تعریف میکنیم:
@section JavaScript
{
<script type="text/javascript" src="@Url.Content("~/Scripts/SomeScript.js")" />;
<script type="text/javascript" src="@Url.Content("~/Scripts/AnotherScript.js")" />;
}
سپس کافی است جهت تزریق این کدها به header تعریف شده در master page مورد نظر، یک سطر زیر را اضافه کرد:
@RenderSection("JavaScript", required: false)
به این ترتیب، اگر view ایی حاوی تعریف قسمت JavaScript نبود، به صورت خودکار شامل تعاریف الحاق اسکریپتهای یاد شده نیز نخواهد گردید. در نتیجه دارای حجمی کمتر و سرعت بارگذاری بالاتری نیز خواهد بود.
مدیریت بهتر فایلها و پوشههای یک برنامه ASP.NET MVC به کمک Areas
به کمک قابلیتی به نام Areas میتوان یک برنامه بزرگ را به چندین قسمت کوچکتر تقسیم کرد. هر کدام از این نواحی، دارای تعاریف مسیریابی، کنترلرها و Viewهای خاص خودشان هستند. به این ترتیب دیگر به یک برنامهی از کنترل خارج شده ASP.NET MVC که دارای یک پوشه Views به همراه صدها زیر پوشه است، نخواهیم رسید و کنترل این نوع برنامههای بزرگ سادهتر خواهد شد.
برای مثال یک برنامه بزرگ را درنظر بگیرید که به کمک قابلیت Areas، به نواحی ویژهای مانند گزارشگیری، قسمت ویژه مدیریتی، قسمت کاربران، ناحیه بلاگ سایت، ناحیه انجمن سایت و غیره، تقسیم شده است. به علاوه هر کدام از این نواحی نیز هنوز میتوانند از اطلاعات ناحیه اصلی برنامه مانند master page آن استفاده کنند. البته باید درنظر داشت که فایل viewStart به پوشه جاری و زیر پوشههای آن اعمال میشود. اگر نیاز باشد تا اطلاعات این فایل به کل برنامه اعمال شود، فقط کافی است آنرا به یک سطح بالاتر، یعنی ریشه سایت منتقل کرد.
نحوه افزودن نواحی جدید
افزودن یک Area جدید هم بسیار ساده است. بر روی نام پروژه در VS.NET کلیک راست کرده و سپس گزینه Add|Area را انتخاب کنید. سپس در صفحه باز شده، نام دلخواهی را وارد نمائید. مثلا نام Reporting را وارد نمائید تا ناحیه گزارشگیری برنامه از قسمتهای دیگر آن مستقل شود. پس از افزودن یک Area جدید، به صورت خودکار پوشه جدیدی به نام Areas به ریشه سایت اضافه میشود. سپس داخل آن، پوشهی دیگری به نام Reporting اضافه خواهد شد. پوشه reporting اضافه شده هم دارای پوشههای Model، Views و Controllers خاص خود میباشد.
اکنون که پوشه Areas به ریشه سایت اضافه شده است، با کلیک راست بر روی این پوشه نیز گزینهی Add|Area در دسترس میباشد. برای نمونه یک ناحیه جدید دیگر را به نام Admin به سایت اضافه کنید تا بتوان امکانات مدیریتی سایت را از سایر قسمتهای آن مستقل کرد.
نحوه معرفی تعاریف مسیریابی نواحی تعریف شده
پس از اینکه کار با Areas را آغاز کردیم، نیاز است تا با نحوهی مسیریابی آنها نیز آشنا شویم. برای این منظور فایل Global.asax.cs قرار گرفته در ریشه اصلی برنامه را باز کنید. در متد Application_Start، متدی به نام AreaRegistration.RegisterAllAreas، کار ثبت و معرفی تمام نواحی ثبت شده را به فریم ورک، به عهده دارد. کاری که در پشت صحنه انجام خواهد شد این است که به کمک Reflection تمام کلاسهای مشتق شده از کلاس پایه AreaRegistration به صورت خودکار یافت شده و پردازش خواهند شد. این کلاسها هم به صورت پیش فرض به نام SomeNameAreaRegistration.cs در ریشه اصلی هر Area توسط VS.NET تولید میشوند. برای نمونه فایل ReportingAreaRegistration.cs تولید شده، حاوی اطلاعات زیر است:
using System.Web.Mvc;
namespace MvcApplication11.Areas.Reporting
{
public class ReportingAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Reporting";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Reporting_default",
"Reporting/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}
توسط AreaName، یک نام منحصربفرد در اختیار فریم ورک قرار خواهد گرفت. همچنین از این نام برای ایجاد پیوند بین نواحی مختلف نیز استفاده میشود.
سپس در قسمت RegisterArea، یک مسیریابی ویژه خاص ناحیه جاری مشخص گردیده است. برای مثال تمام آدرسهای ناحیه گزارشگیری سایت باید با http://localhost/reporting آغاز شوند تا مورد پردازش قرارگیرند. سایر مباحث آن هم مانند قبل است. برای مثال در اینجا نام اکشن متد پیش فرض، index تعریف شده و همچنین ذکر قسمت id نیز اختیاری است.
همانطور که ملاحظه میکنید، تعاریف مسیریابی و اطلاعات پیش فرض آن منطقی هستند و آنچنان نیازی به دستکاری و تغییر ندارند. البته اگر دقت کرده باشید مقدار نام controller پیش فرض، مشخص نشده است. بنابراین بد نیست که مثلا نام Home یا هر نام مورد نظر دیگری را به عنوان نام کنترلر پیش فرض در اینجا اضافه کرد.
تعاریف کنترلرهای هم نام در نواحی مختلف
در ادامه مثال جاری که دو ناحیه Admin و Reporting به آن اضافه شده، به پوشههای Controllers هر کدام، یک کنترلر جدید را به نام HomeController اضافه کنید. همچنین این HomeController را در ناحیه اصلی و ریشه سایت نیز اضافه نمائید. سپس برای متد پیش فرض Index هر کدام هم یک View جدید را با کلیک راست بر روی نام متد و انتخاب گزینه Add view، اضافه کنید. اکنون برنامه را به همین نحو اجرا نمائید. اجرای برنامه با خطای زیر متوقف خواهد شد:
Multiple types were found that match the controller named 'Home'. This can happen if the route that services this
request ('{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request.
If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
The request for 'Home' has found the following matching controllers:
MvcApplication11.Areas.Admin.Controllers.HomeController
MvcApplication11.Controllers.HomeController
فوق العاده خطای کاملی است و راه حل را هم ارائه داده است! برای اینکه مشکل ابهام یافتن HomeController برطرف شود، باید این جستجو را به فضاهای نام هر قسمت از نواحی برنامه محدود کرد (چون به صورت پیش فرض فضای نامی برای آن مشخص نشده، کل ناحیه ریشه سایت و زیر مجموعههای آنرا جستجو خواهد کرد). به همین جهت فایل Global.asax.cs را گشوده و متد RegisterRoutes آنرا مثلا به نحو زیر اصلاح نمائید:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
, namespaces: new[] { "MvcApplication11.Controllers" }
);
}
آرگومان چهارم معرفی شده، آرایهای از نامهای فضاهای نام مورد نظر را جهت یافتن کنترلرهایی که باید توسط این مسیریابی یافت شوند، تعریف میکند.
اکنون اگر مجددا برنامه را اجرا کنیم، بدون مشکل View متناظر با متد Index کنترلر Home نمایش داده خواهد شد.
البته این مشکل با نواحی ویژه و غیر اصلی سایت وجود ندارد؛ چون جستجوی پیش فرض کنترلرها بر اساس ناحیه است.
در ادامه مسیر http://localhost/Admin/Home را نیز در مرورگر وارد کنید. سپس بر روی صفحه در مروگر کلیک راست کرده و سورس صفحه را بررسی کنید. مشاهده خواهید کرد که master page یا فایل layout ایی به آن اعمال نشده است. علت را هم در ابتدای بحث Areas مطالعه کردید. فایل Views\_ViewStart.cshtml در سطحی که قرار دارد به ناحیه Admin اعمال نمیشود. آنرا به ریشه سایت منتقل کنید تا layout اصلی سایت نیز به این قسمت اعمال گردد. البته بدیهی است که هر ناحیه میتواند layout خاص خودش را داشته باشد یا حتی میتوان با مقدار دهی خاصیت Layout نیز در هر view، فایل master page ویژهای را انتخاب و معرفی کرد.
نحوه ایجاد پیوند بین نواحی مختلف سایت
زمانیکه پیوندی را به شکل زیر تعریف میکنیم:
@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home")
یعنی ایجاد لینکی در ناحیه جاری. برای اینکه پیوند تعریف شده به ناحیهای خارج از ناحیه جاری اشاره کند باید نام Area را صریحا ذکر کرد:
@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home",
routeValues: new { Area = "Admin" } , htmlAttributes: null)
همین نکته را باید حین کار با متد RedirectToAction نیز درنظر داشت:
public ActionResult Index()
{
return RedirectToAction("Index", "Home", new { Area = "Admin" });
}
در تصویر بالا دو مسیر با آدرسهای : ShowPage1/ و ShowPage2/ تعریف شده است که هر کدام به یک view مشخص و یک Controller برای مدیریت آن اشاره میکند.
زمانی که ما از تزریق وابستگیها در AngularJs استفاده میکنیم و یک شیء را به کنترلر تزریق میکنیم، Angular توسط Injector$ سعی در پیدا کردن وابستگی مربوطه و سپس تزریق آن به کنترلر را انجام میدهد. برای استفاده از امکان مسیریابی Route ، ما نیز باید از پروایدر مخصوص آن برای تزریق استفاده کنیم. در Angular مسیرهای برنامه توسط پروایدری به نام routeProvider$ شناسایی میشود که خدمات مسیریابی را به ما ارائه میدهد. این سرویس به ما کمک میکند تا بتوانیم اتصال بین کنترلر ها، ویوها و آدرس URL جاری مرورگرها را به آسانی برقرار کنیم.
بهتر است کار را شروع کنیم . یک فایل JS ایجاد و سپس محتویات زیر را در آن قرار دهید :
var myFirstRoute = angular.module('myFirstRoute', []); myFirstRoute.config(['$routeProvider', function($routeProvider) { $routeProvider. when('/pageOne', { templateUrl: 'templates/page_one.html', controller: 'ShowPage1Controller' }). when('/pageTwo', { templateUrl: 'templates/page_two.html', controller: 'ShowPage2Controller' }). otherwise({ redirectTo: '/pageOne' }); }]); myFirstRoute.controller('ShowPage1Controller', function($scope) { $scope.message = 'Content of page-one.html'; }); myFirstRoute.controller('ShowPage2Controller', function($scope) { $scope.message = 'Content of page-two.html'; });
در کدهای بالا ابتدا یک ماژول تعریف کرده ایم و سپس توسط ()config. تنظیمات مربوط به مسیریابی را انجام داده ایم. با استفاده از متدهای when. و otherwise. میتوانیم مسیرها را تعریف کنیم. برای هر مسیر دو پارامتر وجود دارد که اولین پارامتر نام مسیر و دومین پارامتر شامل 2 قسمت میشود که templateUrl آن آدرسی که باز خواهد شد و controller نیز نام کنترلری که ویو را مدیریت میکند.
توسط otherwise میتوانیم مسیر پیشفرض را نیز تعریف کنیم تا درصورتی که مسیری با آدرسهای بالای آن مطابقت نداشت به این آدرس منتقل شود.
در قطعه کد بالا همچنین دو مسیر با نامهای pageOne/ و pageTwo/ تعریف کرده ایم که هر کدام به ترتیب به Viewهای : templates/page_one.html و templates/page_two.html مرتبط شده اند. همچنین دو کنترلر برای مدیریت ویوها نیز تعریف شده است.
زمانی که ما آدرس http://appname/#pageOne را در نوار آدرس مرورگر وارد میکنیم، Angular به صورت اتوماتیک آدرس URL را با تنظیماتی که ما در اینجا تعریف کرده ایم مطابقت میدهد و در صورت وجود چنین آدرسی ، view مربوطه را بارگزاری میکند و در این مثال نیز مطابق با تنظیمات بالا، صفحهی templates/page_one.html برای ما بارگزاری و سپس کنترلر ShowPage1Controller را فراخوانی میکند ، جایی که ما منطق کار را در آن قرار میدهیم.
محتویات فایل main.html :
<body ng-app="app"> <div> <div> <div> <ul> <li><a href="#pageOne"> Show page one </a></li> <li><a href="#pageTwo"> Show page two </a></li> </ul> </div> <div> <div ng-view></div> </div> </div> </div> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script> <script src="app.js"></script> </body>
در قطعه کد بالا دو لینک تعریف شده است که ویژگی href از علامت هش و نام صفحه تشکیل شده است. یکی از چیزهایی که شایان ذکر است ، دایرکتیو ng-view است. مکانی برای بارگزاری صفحات در آن.
ما میتوانیم این تگ را به سه شکل زیر نیز استفاده کنیم :
<div ng-view></div> .. <ng-view></ng-view> .. <div class="ng-view"></div>
محتویات صفحه templates/page_one.html :
<h2>Page One</h2> {{ message }}
محتویات صفحه templates/page_two.html :
<h2>Page Two</h2> {{ message }}
حال اگر پروژه را اجرا کنید و به کنسول مرور گر خود نگاه کنید متوجه میشوید که مسیریاب از مسیر پیشفرض استفاده کرده است و صفحهی page_one.html را به صورت ایجکسی فراخوانی کرده است :
و اگر روی لینک Show Page two کلیک کنید ، صفحهی page_two.html نیز به صورت ایجکسی فراخوانی میشود.
دوباره بر روی لینک Show page one کلیک کنید. بله. هیچ درخواستی به سمت سرور ارسال نشد و صفحهی page_one.html به خوبی نمایش داده شد. یکی از مزیتهای سیستم مسیریابی قابلیت کش کردن صفحات است تا در صورت فراخوانی مجدد، درخواستی به سمت سرور ارسال نشود و خیلی سریع به شما نمایش داده شود.
مثال این مطلب : RouteExample.zip
ادامه دارد ...
SignalR - قسمت دوم
با توجه به تصویر بالا SignalR در شرایط موجود بهترین روش برای برقراری ارتباط با سرور رو forever frame تشخیص داده و مشاهده میشه که این ارتباط دائمیه و فعلا نتیجهای از سمت سرور دریافت نکرده و ارتباط کاملا زنده است. البته اگر در این ابزار درباره درخواستهای ارسالی به سرور بیشتر جستجو بکنین اطلاعات بیشتری نصیبتون میشه که آوردنش اینجا بحث رو طولانی میکنه.
حالا برنامه رو در یه مرورگر دیگه که از html5 پشتیبانی میکنه اجرا کنین. مثلا نتیجه در گوگل کروم و ابزار توسعه اون به شکل زیره:
همونطور که میبینین در اینجا روش استفاده شده Server Sent Events هست.
در فایرفاکس هم با استفاده از ابزار محبوب firebug نتیجه مشابه کروم بدست میاد:
البته اگر علاقه زیادی به کندوکاو در جزئیات این درخواستها دارین (مثل خود من) چیزی بهتر از fiddler2 پیدا نمیشه. میتونین پس از ارسال یک متن دوباره این درخواستها رو مورد بررسی قرار بدین و ببینین که چیجوری کانالهای ارتباط پس از ارسال و دریافت دیتا قطع و برقرار میشه.
این نکته رو هم باید یادآور بشم که هرچند که این کتابخونه بهترین روش رو میتونه انتخاب کنه اما به برنامه نویس امکان تعیین صریح روش ارتباط رو هم میده. اگر به راهنماهای این کتابخونه سر بزنین میبینین که امکانات زیادی بهش اضافه شده و امکانات زیادی هم در آینده به اون اضافه میشه. امکاناتی از قبیل ارسال دادهها به یک کلاینت خاص و یا به گروهی خاص از کلاینتها، خصوصیسازی آدرس سرور و همچنین پشتیبانی از Cross Domain در آخرین نسخه، امکان استفاده از Reactive Extension (بلاگ)، بحث Self Hosting که امکان خیلی جالبیه و میتونه خیلی جاها یه عنوان یک راهحل سبک و سریع به کار بیاد، قابلیت فوق العاده در بایندینگ دادهها در سمت سرور و مخصوصا کلاینت، امکان تشخیص برقراری یا قطع ارتباط کلاینتها در سمت سرور، استفاده از امکانات این کتابخونه برای برقراری ارتباط با کلاینتها در خارج از فضای کلاسهای مشتق شده از دو کلاس پایه (Hub و PersistentConnection) و چند مورد دیگه تا نسخه جاری اضافه شدند.
درحال حاضر دارم روی یه برنامه چت با امکانات بیشتر کار میکنم که پس از آماده شدن ارائه میدمش. یکی از پروژههای متن بازی که با استفاده از این کتابخونه توسعه داده شده jabbr.net است. یه اتاق گفتگوی کامل با امکانات جالبه که میتونین به اون هم یه سری بزنین.
در آخر هم یه لینک جالب برای مطالعه معرفی میکنم: Highest voted Signalr Questions - stackoverflow
قالب ASP.NET Core و Angular 2 برای Visual Studio
{ "projects": [ "src", "test" ], "sdk": { "version": "1.0.0-preview2-003121" } }
@{ ViewData["Title"] = "Home Page"; } <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - WebApplicationBasic</title> <link rel="stylesheet" href="ClientApp/dist/vendor.css" /> </head> <body> <app asp-prerender-module="ClientApp/dist/main-server">Loading...</app> @section scripts { <script src="~/ClientApp/dist/vendor.js"></script> <script src="~/ClientApp/dist/main-client.js"></script> } </body> </html>
تفاوت Bower و npm
<head> <link href="node_modules/normalize.css/normalize.css" rel="stylesheet" /> <link href="node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" /> <link href="node_modules/froala-editor/css/froala_style.min.css" rel="stylesheet" /> <link href="node_modules/froala-editor/css/froala_editor.min.css" rel="stylesheet" /> <script src="node_modules/jquery/dist/jquery.min.js"></script> <script src="node_modules/froala-editor/js/froala_editor.min.js"></script> <script src="node_modules/froala-editor/js/languages/fa.js"></script> </head>
module.exports = function (grunt) { var developer = false; var config = { libPath: 'wwwroot/lib/', nodePath: 'node_modules/', getNodePackagePath: function (path) { return this.nodePath + path; }, getLibPath: function (path) { return this.libPath + path; } } grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), copy: { jquery: { files: [{ cwd: config.getNodePackagePath('jquery/dist/'), expand: true, src: 'jquery.min*', dest: config.getLibPath('jquery/'), }] }, font_awesome: { files: [ { cwd: config.getNodePackagePath('font-awesome/'), expand: true, src: ['css/*.min.css', 'fonts/*'], dest: config.getLibPath('font-awesome/'), filter: 'isFile' }, ] }, froala_editor: { files: [ { cwd: config.getNodePackagePath('froala-editor/'), expand: true, src: ['css/**/*.min.css', 'js/**', '!js/languages/*.js', 'js/languages/fa.js'], dest: config.getLibPath('froala-editor/') } ] }, normalizecss: { files: [ { cwd: config.getNodePackagePath('normalize.css/'), expand: true, src: 'normalize.css', dest: config.getLibPath('normalize.css/') } ] } }, clean: { lib: ['wwwroot/lib/'] }, }); grunt.loadNpmTasks("grunt-contrib-clean"); grunt.loadNpmTasks("grunt-contrib-copy"); grunt.registerTask("default", ['clean', 'copy']); };
<head> <link href="wwwroot/lib/normalize.css/normalize.css" rel="stylesheet" /> <link href="wwwroot/lib/font-awesome/css/font-awesome.min.css" rel="stylesheet" /> <link href="wwwroot/lib/froala-editor/css/froala_style.min.css" rel="stylesheet" /> <link href="wwwroot/lib/froala-editor/css/froala_editor.min.css" rel="stylesheet" /> <script src="wwwroot/lib/jquery/jquery.min.js"></script> <script src="wwwroot/lib/froala-editor/js/froala_editor.min.js"></script> <script src="wwwroot/lib/froala-editor/js/languages/fa.js"></script> </head>
بله، حدس شما درست است استفاده از صفحه کلیدهای مجازی میشه گفت یکی از بهترین راههای ممکن هست، چون در این روشها کلید به صورت سخت افزاری فشرده نمیشود (کلید فشرده شده به صف پیامهای ویندوز نمیرود) در نتیجه نرم افزارها یا سخت افزارهای جاسوس نمیتوانند این اطلاعات را ثبت کنند. و کاربر با خیال راحت میتواند دادههای خود را وارد نمایند (تاکید میکنم این روش فقط جلو این نرم افزارها یا سخت افزارها را میگیرد و تضمینی برای اینکه در زمان ارسال دادههای شما لو نرود ندارد).
خوب حال چه باید کرد؟
یک راه میتواند پیادهسازی صفحه کلید مجازی با کدهای طرف کلاینت مانند جاوا اسکریپت و ویبی اسکریپت است، اما گروهی پلاگینی را توسعه دادهاند که با چند خط کدنویسی ساده به راحتی میتوانید یک صفحه کلید مجازی چندزبانه (با هر زبانی که دلتون میخواد) داشته باشید و از اون در برنامههای خودتون استفاده کنید.
نحوهی نصب:
ایتدا فایلهای مورد نیاز را از سایت سازنده که شامل فایل جاوا اسکریپت ، فایل استایل و یک تصویر (آخرین نسخه) یا از این آدرس به صورت کامل (در حال حاضر نسخه 1.49) دریافت کرده، پس از دریافت فایلها آنها را در هاست خود بارگزاری (آپلود) نمائید. سپس کدهای زیر را در صفحهای که میخواهید صفحه کلید نمایش یابد در بین تگ <head> ... و <head/> قرار دهید.
<script type="text/javascript" src="keyboard.js" charset="UTF-8"></script> <link rel="stylesheet" type="text/css" href="keyboard.css">
مثال:
<input type="text" value="" class="keyboardInput">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript" src="keyboard.js" charset="UTF-8"></script> <link rel="stylesheet" type="text/css" href="keyboard.css"/> </head> <body> <input type="text" value="" class="keyboardInput"/> </body> </html>
با این کار پس از اجرای صفحه مورد نظر خروجی شما مانند تصویر زیر خواهد بود، جهت محدود کردن کلیدها و عملیات دلخواه و سفارشی سازی با پارامترهای دلخواه میتواند از دموهای موجود در سایت سازنده بهره بگیرید.
موفق وموید باشید.