مطابق Ajax API ترجمه گوگل، برای ترجمه یک متن باید محتویات آدرس زیر را تحلیل کرد:
http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q={0}&langpair={1}|{2}
بنابراین برای استفاده از آن تنها کافی است این URL را تشکیل داده و سپس محتویات خروجی آنرا آنالیز کرد. فرمت نهایی دریافت شده از نوع JSON است. برای مثال اگر hello world! را به این سرویس ارسال نمائیم، خروجی نهایی JSON دریافت شده به صورت زیر خواهد بود:
//{\"responseData\": {\"translatedText\":\"سلام جهان!\"}, \"responseDetails\": null, \"responseStatus\": 200}
در کتابخانهی System.Web.Extensions.dll دات نت فریم ورک سه و نیم، کلاس JavaScriptSerializer برای این منظور پیش بینی شده است. تنها کافی است به متد Deserialize آن، متن JSON دریافتی را پاس کنیم:
GoogleAjaxResponse result =
new JavaScriptSerializer().Deserialize<GoogleAjaxResponse>(jsonGoogleAjaxResponse);
برای اینکه عملیات نگاشت اطلاعات متنی JSON به کلاسهای دات نتی ما با موفقیت صورت گیرد، میتوان خروجی JSON گوگل را به شکل زیر نمایش داد:
//ResponseData.cs file
public class ResponseData
{
public string translatedText { get; set; }
}
//GoogleAjaxResponse.cs file
using System.Net;
/// <summary>
/// کلاسی جهت نگاشت اطلاعات جی سون دریافتی به آن
/// </summary>
public class GoogleAjaxResponse
{
public ResponseData responseData { get; set; }
public object responseDetails { get; set; }
public HttpStatusCode responseStatus { get; set; }
}
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Web;
using System.Web.Script.Serialization;
//{\"responseData\": {\"translatedText\":\"سلام جهان!\"}, \"responseDetails\": null, \"responseStatus\": 200}
public class CGoogleTranslator
{
#region Fields (1)
/// <summary>
/// ارجاع دهنده
/// </summary>
private readonly string _referrer;
#endregion Fields
#region Constructors (1)
/// <summary>
/// مطابق مستندات نیاز به یک ارجاع دهنده اجباری میباشد
/// </summary>
/// <param name="referrer"></param>
public CGoogleTranslator(string referrer)
{
_referrer = referrer;
}
#endregion Constructors
#region Properties (2)
/// <summary>
/// ترجمه از زبان
/// </summary>
public CultureInfo FromLanguage { get; set; }
/// <summary>
/// ترجمه به زبان
/// </summary>
public CultureInfo ToLanguage { get; set; }
#endregion Properties
#region Methods (2)
// Public Methods (1)
/// <summary>
/// ترجمه متن با استفاده از موتور ترجمه گوگل
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public string TranslateText(string data)
{
//ساخت و انکدینگ آدرس مورد نظر
string url =
string.Format(
"http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q={0}&langpair={1}|{2}",
HttpUtility.UrlEncode(data), //needs a ref. to System.Web.dll
FromLanguage.TwoLetterISOLanguageName,
ToLanguage.TwoLetterISOLanguageName
);
//دریافت اطلاعات جی سون از گوگل
string jsonGoogleAjaxResponse = fetchWebPage(url);
//needs a ref. to System.Web.Extensions.dll
//نگاشت اطلاعات جی سون دریافت شده به کلاس مرتبط
GoogleAjaxResponse result =
new JavaScriptSerializer().Deserialize<GoogleAjaxResponse>(jsonGoogleAjaxResponse);
if (result != null && result.responseData != null && result.responseStatus == HttpStatusCode.OK)
{
return result.responseData.translatedText;
}
return string.Empty;
}
// Private Methods (1)
/// <summary>
/// دریافت محتویات جی سون بازگشتی از گوگل
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
string fetchWebPage(string url)
{
try
{
var uri = new Uri(url);
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
var request = WebRequest.Create(uri) as HttpWebRequest;
if (request != null)
{
request.Method = WebRequestMethods.Http.Get;
request.Referer = _referrer;
request.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.0; ; rv:1.8.0.7) Gecko/20060917 Firefox/1.9.0.1";
request.AllowAutoRedirect = true;
request.Timeout = 1000 * 300;
request.KeepAlive = false;
request.ReadWriteTimeout = 1000 * 300;
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
using (var response = request.GetResponse() as HttpWebResponse)
{
if (response != null)
{
using (var reader = new StreamReader(response.GetResponseStream()))
{
return reader.ReadToEnd().Trim();
}
}
}
}
}
return string.Empty;
}
catch (Exception ex)
{
Console.WriteLine(String.Format("fetchWebPage: {0} >> {1}", ex.Message, url), true);
return string.Empty;
}
}
#endregion Methods
}
string res = new CGoogleTranslator("https://www.dntips.ir/")
{
FromLanguage = CultureInfo.GetCultureInfo("en-US"),
ToLanguage = CultureInfo.GetCultureInfo("fa-IR")
}.TranslateText("Hello world!");
- Convert.ToDateTime()
- DateTime.Parse()
- DateTime.ParseExact()
- DateTime.TryParse()
- DateTime.TryParseExact()
CultureInfo culture = new CultureInfo("en-US"); DateTime tempDate = Convert.ToDateTime("1/1/2010 12:10:15 PM", culture);
string datestr = null; Console.WriteLine(Convert.ToDateTime(datestr));//0001-01-01T00:00:00 datestr = "wrong string"; Console.WriteLine(Convert.ToDateTime(datestr)); //Unhandled Exception: System.FormatException: //The string was not recognized as a valid DateTime. //There is an unknown word starting at index 0. datestr = "Tue Dec 30,2015"; Console.WriteLine(Convert.ToDateTime(datestr)); //Unhandled Exception: System.FormatException: //String was not recognized as a valid DateTime.
تابع () DateTime.Parse :
- DateTime.Parse(String value, IFormatProvider provider, DateTypeStyles styles)
string datestr = null; Console.WriteLine(DateTime.Parse(datestr)); //// Exception: Argument null exception datestr = "wrong string"; Console.WriteLine(DateTime.Parse(datestr)); //// Exception: The string was not recognized as a valid DateTime. //// There is an unknown word starting at index 0. datestr = "Tue Dec 30, 2015"; //Unhandled Exception: System.FormatException: //String was not recognized as a valid DateTime. Console.WriteLine(DateTime.Parse(datestr));
تابع ()DateTime.ParsExact
string datestr = null; CultureInfo provider = CultureInfo.InvariantCulture; Console.WriteLine(DateTime.ParseExact(datestr, "mm/dd/yyyy", provider)); //Unhandled Exception: System.ArgumentNullException: //String reference not set to an instance of a String. datestr = "wrong date"; Console.WriteLine(DateTime.ParseExact(datestr, "mm/dd/yyyy", provider)); //Unhandled Exception: System.FormatException: //String was not recognized as a valid DateTime datestr = "Tue Dec 30, 2015"; Console.WriteLine(DateTime.ParseExact(datestr, "mm/dd/yyyy", provider)); //Unhandled Exception: System.FormatException: //String was not recognized as a valid DateTime. datestr = "10-22-2015"; Console.WriteLine(DateTime.ParseExact(datestr, "MM-dd-yyyy", provider)); //30/07/1394 12:00:00 ق.ظ datestr = "10-22-2015"; Console.WriteLine(DateTime.ParseExact(datestr, new string[] {"MM-dd-yyyy", "MM/dd/yyyy", "MM.dd.yyyy"}, provider, DateTimeStyles.None)); //30/07/1394 12:00:00 ق.ظ
تابع ()DateTime.TryParse
string datestr = null; DateTime temp; Console.WriteLine(DateTime.TryParse(datestr, out temp)); Console.WriteLine(temp); //False //0001 - 01 - 01T00: 00:00 datestr = "wrong date"; Console.WriteLine(DateTime.TryParse(datestr, out temp)); Console.WriteLine(temp); //False //0001 - 01 - 01T00: 00:00 datestr = "Tue Dec 30, 2015"; Console.WriteLine(DateTime.TryParse(datestr, out temp)); Console.WriteLine(temp); //False //0001 - 01 - 01T00: 00:00
تابع ()DateTime.TryParseExact
string datestr = null; DateTime temp; CultureInfo provider = CultureInfo.InvariantCulture; Console.WriteLine(DateTime.TryParseExact(datestr, "MM/dd/yyyy", provider, DateTimeStyles.None, out temp)); Console.WriteLine(temp); //False //1/1/0001 12:00:00 AM datestr = "wrong date"; Console.WriteLine(DateTime.TryParseExact(datestr, "MM/dd/yyyy", provider, DateTimeStyles.None, out temp)); Console.WriteLine(temp); //False //1/1/0001 12:00:00 AM datestr = "Tue Dec 30, 2015"; Console.WriteLine(DateTime.TryParseExact(datestr, "MM/dd/yyyy", provider, DateTimeStyles.None, out temp)); Console.WriteLine(temp); //False //1/1/0001 12:00:00 AM datestr = "10‐22‐2015"; Console.WriteLine(DateTime.TryParseExact(datestr, "MM/dd/yyyy", provider, DateTimeStyles.None, out temp)); Console.WriteLine(temp); //False //1/1/0001 12:00:00 AM datestr = "10‐22‐2015"; Console.WriteLine(DateTime.TryParseExact( datestr, "MM‐dd‐yyyy", provider, DateTimeStyles.None, out temp)); Console.WriteLine(temp); //True //30/07/1394 12:00:00 ق.ظ datestr = "10‐12‐2015"; Console.WriteLine(DateTime.TryParseExact(datestr, new string[] { "MM/dd/yyyy", "MM‐dd‐yyyy", "MM.dd.yyyy" }, provider, DateTimeStyles.None, out temp)); Console.WriteLine(temp); //True //20/07/1394 12:00:00 ق.ظ
آنالیز متدهای معرفی شده
به طور خلاصه میتوانید امکانات متعدد این پلاگین را در زیر مشاهده کنید:
- صفحه بندی دادهها با تعداد رکوردهای قابل تغییر در هر صفحه (variable length pagination)
- فیلتر کردن دادههای بایند شده به جدول (on-the-fly filtering)
- مرتب سازی دادهها بر اساس ستونهای مختلف با قابلیت تشخیص نوع داده ستون (Multi-column sorting with data type detection)
- تغییر اندازه ستونها به صورت هوشمند (Smart handling of column widths)
- نمایش دادهها در جدول از اکثر data sourceها (DOM، یک آرایه جاوا اسکریپتی، یک فایل، یا با استفاده از پردازش سمت سروری (سی شارپ، php و غیره) )
- قابلیت جهانی شدن یا منطبق شدن با زبانهای مختلف دنیا (Fully Internationalisable)
- قابلیت تعویض theme آن با استفاده از jQuery UI ThemeRoller
- وجود داشتن 2900 آزمون واحد برای آن (backed by a suite of 2900 unit test)
- وجود داشتن پلاگینهای متعدد برای آن
- و رایگان بودن آن
برای استفاده از این پلاگین ابتدا به اینجا مراجعه کرده و آنرا به همراه مثالهای آن که در یک فایل فشرده هستند را دانلود کنید. بعد از دانلود و خارج کردن فایل دانلودی از حالت فشرده، وارد پوشه examples از آن که بشوید میتوانید مثالهای متعدد در رابطه با این پلاگین را مشاهده نمائید.
مثالهای این پلاگین یکی از بهترین منابع یادگیری آن هستند. در این سری از مقالات هم از روی همین مثالها پیش میرویم. برای این کار، بعد از مراجعه به پوشه examples فایل index.html را باز کنید و مثال اول را (Zero Configuration) کلیک کنید.
نتیجه حاصل از اجرای مثال Zero Configuration چیزی شبیه تصویر زیر است:
تصویر را شماره گزاری کرده ام تا بتوانم راحتتر آنرا برایتان تشریح کنم.
- دادههای درون جدول (10 تای اول) که در قسمت tbody جدول قرار دارند
- قسمت thead جدول
- قسمت tfoot جدول
- اندازه صفحه (page size)
- کادر جستجو که در کلیه ستونهای جدول جستجویی را انجام میدهد و دادهها بر اساس آن فیلتر میشوند.
- قابلیت مرتب سازی رکوردها بر اساس یک ستون خاص به صورت صعودی یا نزولی
- اطلاعات مربوط به رکوردهای جاری و تعداد کل رکوردها
- قابلیت تغییر صفحه با دکمههای previous و next
تشریح مثال Zero Configuration :
برای استفاده از این پلاگین، باید ارجاعی به کتابخانه jquery و نیز فایل jquery.dataTables.js وجود داشته باشد. این دو فایل در زیر پوشه media/js قرار گرفته اند.
<script type="text/javascript" language="javascript" src="../../media/js/jquery.js"></script> <script type="text/javascript" language="javascript" src="../../media/js/jquery.dataTables.js"></script>
<style type="text/css" title="currentStyle"> @import "../../media/css/demo_page.css"; @import "../../media/css/demo_table.css"; </style>
این نکته هم جا نمونه که برای اعمال شدن پلاگین DataTables به یک جدول که به طور مثال id جدول example هست، به صورت زیر عمل میکنیم:
$(document).ready(function() { $('#example').dataTable(); } );
بررسی ساختار کامپوننت Pagination
شبیه به کامپوننت Like که در قسمت قبل ایجاد کردیم، میخواهیم کامپوننت جدید Pagination نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\pagination.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد میکنیم. هرچند میتوان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت میکند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import میکنیم:
import Pagination from "./common/pagination";
<Pagination itemsCount={this.state.movies.length} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} />
state = { movies: getMovies(), pageSize: 4 };
handlePageChange = page => { console.log("handlePageChange", page); };
نمایش شماره صفحات گرید، در کامپوننت صفحه بندی
برای رندر کامپوننت صفحه بندی، از کلاسهای مخصوص اینکار که در بوت استرپ تعریف شدهاند، استفاده میکنیم که ساختار کلی آن به صورت زیر است و از یک المان nav که داخل آن ul ای با کلاس pagination و liهایی با کلاس page-item هستند، تشکیل میشود. هر li، به همراه یک anchor است؛ با کلاس page-link تا لینک به صفحهای خاص را ارائه دهد که در اینجا بجای لینک، از کلیک بر روی آنها استفاده خواهیم کرد:
import React, { Component } from "react"; class Pagination extends Component { render() { return ( <nav> <ul className="pagination"> <li className="page-item"> <a className="page-link">1</a> </li> </ul> </nav> ); } } export default Pagination;
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، عدد 1 را در پایین جدول فیلمها مشاهده خواهید کرد:
اکنون باید رندر این liها را بر اساس ورودیهای این کامپوننت که پیشتر معرفی کردیم، یعنی pageSize و itemsCount، پویا کنیم. به همین جهت نیاز به آرایهای داریم که بر اساس این ورودیها، شمارهی صفحات مانند [1,2,3] را ارائه دهد تا بر روی آن متد Array.map را فراخوانی کرده و liهای مورد نیاز را به صورت پویا رندر کنیم:
class Pagination extends Component { // ... getPageNumbersArray() { const { itemsCount, pageSize } = this.props; const pagesCount = Math.ceil(itemsCount / pageSize); if (pagesCount === 1) { return null; } const pages = new Array(); for (let i = 1; i <= pagesCount; i++) { pages.push(i); } return pages; } }
سپس به کمک متد map، اعضای این آرایه را تبدیل به لیست liهای نمایش شماره صفحات میکنیم. در اینجا key هر li را نیز به شماره صفحه که منحصربفرد است، تنظیم کردهایم:
class Pagination extends Component { render() { const pages = this.getPageNumbersArray(); if (!pages) { return null; } return ( <nav> <ul className="pagination"> {pages.map(page => ( <li key={page} className="page-item"> <a className="page-link">{page}</a> </li> ))} </ul> </nav> ); }
پس از ذخیرهی این کامپوننت و بارگذاری مجدد برنامه در مرورگر، شمارهی صفحات رندر شده، در پایین جدول مشخص هستند:
با داشتن 9 فیلم در آرایهی movies و نمایش 4 فیلم به ازای هر صفحه، به 3 صفحه خواهیم رسید که به درستی در اینجا رندر شدهاست. یکبار هم برای آزمایش بیشتر، مقدار pageSize را در کامپوننت movies به 10 تنظیم کنید. در این حالت کامپوننت صفحه بندی نباید رندر شود.
مدیریت انتخاب شمارههای صفحات
در این قسمت میخواهیم مدیریت onPageChange={this.handlePageChange} را که به تعریف المان صفحه بندی در کامپوننت movies اضافه کردیم، تکمیل کنیم. برای این منظور در کامپوننت صفحه بندی، قسمت anchor را به صورت زیر تغییر میدهیم تا با کلیک بر روی آن، رخداد onPageChange صادر شود:
<a onClick={() => this.props.onPageChange(page)} className="page-link" style={{ cursor: "pointer" }} > {page} </a>
تا اینجا اگر برنامه را آزمایش کنیم، با کلیک بر روی لینکهای شماره صفحات، شماره صفحهی انتخابی، در کنسول توسعه دهندگان مرورگر لاگ میشود.
اکنون میخواهیم اگر صفحهای انتخاب شد، شمارهی آن صفحه با رنگی دیگر نمایش داده شود. در بوت استرپ برای اینکار تنها کافی است کلاس active را به className هر li اضافه کنیم و برعکس. یعنی اگر page ای مساوی صفحهی جاری انتخاب شده بود (currentPage در اینجا)، آنگاه کلاس page-item active، به المان li اضافه شود. بنابراین در این کامپوننت نیاز است عدد currentPage را نیز دریافت کنیم. به همین جهت ویژگی currentPage را به تعریف المان Pagination در کامپوننت movies اضافه میکنیم:
<Pagination itemsCount={this.state.movies.length} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} currentPage={this.state.currentPage} />
class Movies extends Component { state = { movies: getMovies(), pageSize: 4, currentPage: 1 };
handlePageChange = page => { console.log("handlePageChange", page); this.setState({currentPage: page}); };
پس از این تغییرات، به کامپوننت صفحه بندی مراجعه کرده و بر اساس currentPage دریافتی، کلاس active را به المان li اعمال میکنیم:
<li key={page} className={ page === this.props.currentPage ? "page-item active" : "page-item" } >
در اولین بار نمایش برنامه، عدد 1 در حالت انتخاب شده قرار دارد؛ چون مقدار currentPage موجود در state، همان عدد 1 است. پس از آن با کلیک بر روی اعداد دیگر، با به روز رسانی state، مقدار currentPage تغییر کرده و کامپوننت صفحه بندی نسبت به آن واکنش نشان میدهد.
نمایش صفحه بندی شدهی اطلاعات
تا اینجا لیستی که نمایش داده میشود، حاوی تمام اطلاعات آرایهی this.state.movies است و بر اساس شمارهی صفحهی انتخابی، تغییر نمیکند. به همین جهت با استفاده از متد slice، تکهای از آرایهی movies را که بر اساس شماره صفحهی انتخابی و تعداد ردیفها در هر صفحه نیاز است نمایش داده شود، انتخاب کرده و بازگشت میدهیم:
paginate() { const first = (this.state.currentPage - 1) * this.state.pageSize; const last = first + this.state.pageSize; return this.state.movies.slice(first, last); }
render() { const { length: count } = this.state.movies; if (count === 0) return <p>There are no movies in the database.</p>; const movies = this.paginate();
پس از ذخیرهی تغییرات و بارگذاری مجدد برنامه، اکنون میتوان نمایش صفحه بندی شدهی اطلاعات را شاهد بود:
بررسی صحت نوع پارامترهای ارسالی به کامپوننتها
تا اینجا فرض بر این است که مصرف کنندهی کامپوننت صفحه بندی، دقیقا همان ویژگیهایی را که ما طراحی کردهایم، با همان نامها و همان نوعها را حتما به آن ارسال میکند. همچنین اگر افزونهی eslint را هم در VSCode نصب کرده باشید، به همراه نصب وابستگیهای زیر در خط فرمان:
> npm i -g typescript eslint tslint eslint-plugin-react-hooks jshint babel-eslint eslint-plugin-react eslint-plugin-mocha
به ازای هر خاصیت props استفاده شدهی در کامپوننت صفحه بندی، اخطاری را مانند «'currentPage' is missing in props validation eslint(react/prop-types)» مشاهده خواهید کرد:
که عنوان میکند props validation این خاصیت استفاده شده، فراموش شدهاست.
در نگارشهای قبلی React، امکانات بررسی نوعهای ارسالی به کامپوننتها، جزئی از بستهی اصلی آن بود؛ اما از نگارش 15 به بعد، به بستهی مستقلی منتقل شدهاست که باید به صورت جداگانهای در ریشهی پروژه نصب شود:
> npm i prop-types --save
البته اگر TypeScript را بر روی سیستم خود نصب کرده باشید، دیگر نیازی به نصب بستهی npm فوق را ندارید و prop-types، جزئی از آن است که عموما در یک چنین مسیری قرار دارد و برای کار کردن با آن، تنها ذکر import مرتبط با PropType در ماژولهای برنامه کافی بوده و برنامه در این حالت بدون مشکل کامپایل میشود:
C:/Users/{username}/AppData/Local/Microsoft/TypeScript/3.6/node_modules/@types/prop-types/index
اکنون در ابتدای فایل کامپوننت صفحه بندی، تعریف زیر را اضافه میکنیم:
import PropTypes from "prop-types";
Pagination.propTypes = { itemsCount: PropTypes.number.isRequired, pageSize: PropTypes.number.isRequired, currentPage: PropTypes.number.isRequired, onPageChange: PropTypes.func.isRequired }; export default Pagination;
سپس مقدار این خاصیت جدید را به شیءای تنظیم میکنیم که نام خواص آن، دقیقا همان نام خواص و رویدادهای props استفاده شدهی در این کامپوننت است. در ادامه توسط PropTypes ای که در ابتدای ماژول import میشود، کار تعریف نوع این خواص و اجباری بودن آنها را میتوان مشخص کرد که برای مثال در اینجا سه خاصیت تعریف شده از نوع عددی و اجباری بوده و onPageChange، از نوع func است.
پس از اضافه کردن Pagination.propTypes و مقدار دهی آن، خطاهای eslint ای که در تصویر فوق مشاهده کردید، برطرف میشوند. همچنین اگر فراخوان کامپوننت Pagination مثلا بجای itemsCount یک رشتهی فرضی را وارد کند (برای آزمایش آن در کامپوننت movies، در تعریف المان Pagination، مقدار itemsCount را یک رشته وارد کنید)، چنین خطایی در مرورگر ظاهر خواهد شد که عنوان میکند itemsCount یک عدد را میپذیرد و نوع ورودی آن اشتباه است:
البته این خطا فقط در حالت development مشاهده میشود و در حالت توزیع برنامه، خیر.
بنابراین تعریف propTypes یک best practice ایجاد کامپوننتهای React است که نه فقط بررسی نوعها را فعال میکند، بلکه میتواند به عنوان مستندات آن نیز در جهت تعیین props مورد نیاز، همچنین نوع و اجباری بودن آنها، اطلاعات کاملی را ارائه کند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-11.zip
پیشنیازهای کار با کامپوننت ng2-file-upload
برای شروع به کار با کامپوننت ng2-file-upload، ابتدا نیاز است بستهی npm آنرا نصب کرد:
>npm install ng2-file-upload --save
همچنین یک کامپوننت آزمایشی را هم به برنامه (دقیقا همان مثال مطلب قبلی) جهت اعمال آن اضافه میکنیم:
>ng g c UploadFile/ng2-file-upload-test
پس از آن نیاز است به ماژولی که این کامپوننت جدید در آن قرار دارد، مدخل FileUploadModule کامپوننت ng2-file-upload را افزود:
import { FileUploadModule } from "ng2-file-upload"; @NgModule({ imports: [ FileUploadModule ]
تکمیل Ng2FileUploadTestComponent جهت اعمال ng2-file-upload
اکنون به کلاس کامپوننت جدیدی که ایجاد کردیم، مراجعه کرده و تغییرات ذیل را اعمال میکنیم:
import { FileUploader, FileUploaderOptions } from "ng2-file-upload"; import { Ticket } from "./../ticket"; export class Ng2FileUploadTestComponent implements OnInit { fileUploader: FileUploader; model = new Ticket();
وهله سازی از کامپوننت ng2-file-upload و انجام تنظیمات اولیهی آن
پس از تعریف خاصیت عمومی fileUploader، اکنون نوبت به وهله سازی آن است:
this.fileUploader = new FileUploader( <FileUploaderOptions>{ url: "api/SimpleUpload/SaveTicket", headers: [ { name: "X-XSRF-TOKEN", value: this.getCookie("XSRF-TOKEN") }, { name: "Accept", value: "application/json" } ], isHTML5: true, // allowedMimeType: ["image/jpeg", "image/png", "application/pdf", "application/msword", "application/zip"] allowedFileType: [ "application", "image", "video", "audio", "pdf", "compress", "doc", "xls", "ppt" ], removeAfterUpload: true, autoUpload: false, maxFileSize: 10 * 1024 * 1024 } );
- اگر برنامه از نکات anti-forgery token استفاده میکند، این کامپوننت برخلاف روش مطرح شدهی در مطلب مشابه قبلی، هیچ هدری را به سمت سرور ارسال نمیکند. بنابراین نیاز است کوکی مرتبط را خودمان یافته و سپس به لیست هدرها اضافه کنیم. در اینجا روش استخراج یک کوکی را توسط کدهای جاوا اسکریپتی مشاهده میکنید:
getCookie(name: string): string { const value = "; " + document.cookie; const parts = value.split("; " + name + "="); if (parts.length === 2) { return decodeURIComponent(parts.pop().split(";").shift()); } }
- برای محدود سازی فایلهای ارسالی توسط این کامپوننت، دو روش وجود دارد:
الف) مشخص سازی مقدار خاصیت allowedMimeType
همانطور که مشاهده میکنید، در اینجا باید mime type فایلهای مجاز را مشخص کرد.
ب) مشخص سازی مقدار خاصیت allowedFileType
برخلاف تصور، در اینجا از پسوند فایلها استفاده نمیکند و از یک لیست از پیش مشخص که نمونهای از آنرا در اینجا مشاهده میکنید، کمک گرفته میشود. بنابراین اگر برای مثال تنها نیاز به ارسال تصاویر بود، مقدار image را نگه داشته و مابقی را از لیست حذف کنید.
- removeAfterUpload به این معنا است که آیا لیست نهایی که نمایش داده میشود، پس از آپلود باقی بماند یا خیر؟
- توسط خاصیت maxFileSize میتوان حداکثر اندازهی قابل قبول فایلهای ارسالی را مشخص کرد.
مدیریت رخدادهای کامپوننت ng2-file-upload
اکنون که وهلهای از این کامپوننت ساخته شدهاست، میتوان رخدادهای آنرا نیز مدیریت کرد. برای مثال:
الف) نحوهی ارسال اطلاعات اضافی به همراه یک فایل به سمت سرور
this.fileUploader.onBuildItemForm = (fileItem, form) => { for (const key in this.model) { if (this.model.hasOwnProperty(key)) { form.append(key, this.model[key]); } } };
ب) اطلاع یافتن از رخداد خاتمهی کار
رخداد onCompleteAll پس از ارسال تمام فایلها به سمت سرور فراخوانی میشود:
this.fileUploader.onCompleteAll = () => { // clear the form // this.model = new Ticket(); };
ج) در حین وهله سازی fileUploader، تعدادی محدودیت نیز قابل اعمال هستند. این محدودیتها سبب نمایش هیچگونه پیام خطایی نمیشوند. فقط زمانیکه کاربر فایلی را انتخاب میکند، این فایل در لیست ظاهر نمیشود. اگر علاقمند به مدیریت این وضعیت باشید، میتوان از رخداد onWhenAddingFileFailed استفاده کرد:
this.fileUploader.onWhenAddingFileFailed = (item, filter, options) => { // msg: `You can't select ${item.name} file because of the ${filter.name} filter.` };
د) اگر ارسال فایلی به سمت سرور با شکست مواجه شود، در رخدادگردان onErrorItem میتوان به نام این فایل و اطلاعات بیشتری که از سمت سرور دریافت شدهاست، دسترسی یافت:
this.fileUploader.onErrorItem = (fileItem, response, status, headers) => { // };
ه) اگر از سمت سرور اطلاعات JSON مانندی یا هر اطلاعات دیگری به سمت کلاینت پس از آپلود ارسال میشود، این اطلاعات را میتوان در رخدادگردان onSuccessItem دریافت کرد:
this.fileUploader.onSuccessItem = (item, response, status, headers) => { if (response) { const ticket = JSON.parse(response); console.log(`ticket:`, ticket); } };
ارسال نهایی فرم و فایلها به سمت سرور
در پایان، با فراخوانی متد uploadAll شیء fileUploader جاری، میتوان اطلاعات فرم و تمام فایلهای آنرا به سمت سرور ارسال کرد:
submitForm(form: NgForm) { this.fileUploader.uploadAll(); // NOTE: Upload multiple files in one request -> https://github.com/valor-software/ng2-file-upload/issues/671 }
کدهای کامل کامپوننت ng2-file-upload-test.component.ts را در اینجا میتوانید مشاهده کنید.
تکمیل قالب کامپوننت Ng2FileUploadTestComponent
اکنون که کار تکمیل کامپوننت آزمایشی ارسال فایلها به سمت سرور به پایان رسید، نوبت به تکمیل قالب آن است.
افزودن فیلد اضافی توضیحات به فرم
<div class="container"> <h3>Support Form(ng2-file-upload)</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="description.invalid && description.touched"> <label class="control-label">Description</label> <input #description="ngModel" required type="text" class="form-control" name="description" [(ngModel)]="model.description"> <div *ngIf="description.invalid && description.touched"> <div class="alert alert-danger" *ngIf="description.errors.required"> description is required. </div> </div> </div>
تعریف ویژهی فیلد ارسال فایلها به سمت سرور
<div class="form-group"> <label class="control-label">Screenshot(s)</label> <input required type="file" multiple ng2FileSelect [uploader]="fileUploader" class="form-control" name="screenshot"> </div>
ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده میکنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.
نمایش لیست فایلها و نمایش درصد پیشرفت آپلود آنها
جدولی را که در تصویر ابتدای بحث مشاهده کردید، به صورت ذیل شکل میگیرد (کدهای آن در همان صفحهی توضیحات کامپوننت نیز موجود هستند):
<div style="margin-bottom: 10px" *ngIf="fileUploader.queue.length"> <h3>Upload queue</h3> <p>Queue length: {{ fileUploader?.queue?.length }}</p> <table class="table"> <thead> <tr> <th width="50%">Name</th> <th>Size</th> <th>Progress</th> <th>Status</th> <th>Actions</th> </tr> </thead> <tbody> <tr *ngFor="let item of fileUploader.queue"> <td><strong>{{ item?.file?.name }}</strong></td> <td nowrap>{{ item?.file?.size/1024/1024 | number:'.2' }} MB</td> <td> <div class="progress" style="margin-bottom: 0;"> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div> </div> </td> <td class="text-center"> <span *ngIf="item.isError"><i class="glyphicon glyphicon-remove"></i></span> </td> <td nowrap> <button type="button" class="btn btn-danger btn-xs" (click)="item.remove()"> <span class="glyphicon glyphicon-trash"></span> Remove </button> </td> </tr> </tbody> </table> <div> <div> Queue progress: <div class="progress"> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': fileUploader.progress + '%' }"></div> </div> </div> <button type="button" class="btn btn-danger btn-s" (click)="fileUploader.clearQueue()" [disabled]="!fileUploader.queue.length"> <span class="glyphicon glyphicon-trash"></span> Remove all </button> </div> </div>
در اینجا از progress-bar بوت استرپ برای نمایش درصد آپلود فایلها استفاده شدهاست:
<div class="progress" style="margin-bottom: 0;"> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div> </div>
غیرفعال کردن دکمهی ارسال، در صورت عدم انتخاب یک فایل
<button class="btn btn-primary" [disabled]="form.invalid || !fileUploader.queue.length" type="submit">Submit</button> </form>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
//برای ذخیره مقادیر از ساختار نام و مقدار استفاده میکنیم که نامها را اینجا ثبت کرده ام var Variables={ posts:"posts", postsComments:"postsComments", shares:"shares", sharesComments:"sharesComments", } //برای ذخیره زمان آخرین تغییر سایت برای هر یک از مطالب به صورت جداگانه نیاز به یک ساختار نام و مقدار است که نامها را در اینجا ذخیره کرده ام var DateContainer={ posts:"dtposts", postsComments:"dtpostsComments", shares:"dtshares", sharesComments:"dtsharesComments", interval:"interval" } //برای نمایش پیامها به کاربر var Messages={ SettingsSaved:"تنظیمات ذخیره شد", SiteUpdated:"سایت به روز شد", PostsUpdated:"مطلب ارسالی جدید به سایت اضافه شد", CommentsUpdated:"نظری جدیدی در مورد مطالب سایت ارسال شد", SharesUpdated:"اشتراک جدید به سایت ارسال شد", SharesCommentsUpdated:"نظری برای اشتراکهای سایت اضافه شد" } //لینکهای فید سایت var Links={ postUrl:"https://www.dntips.ir/feeds/posts", posts_commentsUrl:"https://www.dntips.ir/feeds/comments", sharesUrl:"https://www.dntips.ir/feed/news", shares_CommentsUrl:"https://www.dntips.ir/feed/newscomments" } //لینک صفحات سایت var WebLinks={ Home:"https://www.dntips.ir", postUrl:"https://www.dntips.ir/postsarchive", posts_commentsUrl:"https://www.dntips.ir/commentsarchive", sharesUrl:"https://www.dntips.ir/newsarchive", shares_CommentsUrl:"https://www.dntips.ir/newsarchive/comments" }
chrome.runtime.onInstalled.addListener(function(details) { var now=String(new Date()); var params={}; params[Variables.posts]=true; params[Variables.postsComments]=false; params[Variables.shares]=false; params[Variables.sharesComments]=false; params[DateContainer.interval]=1; params[DateContainer.posts]=now; params[DateContainer.postsComments]=now; params[DateContainer.shares]=now; params[DateContainer.sharesComments]=now; chrome.storage.local.set(params, function() { if(chrome.runtime.lastError) { /* error */ console.log(chrome.runtime.lastError.message); return; } }); });
chrome.storage.local.set('mykey':myvalue,....
chrome.storage.local.set(mykey:myvalue,...
"background": { "scripts": ["const.js","init.js"] }
نکته:نمی توان در تعریف بک گراند هم فایل اسکریپت معرفی کرد و هم فایل html
"background": { "page": "background.htm" }
<html> <head> <script type="text/javascript" src="const.js"></script> <script type="text/javascript" src="https://www.google.com/jsapi"></script> <script type="text/javascript" src="init.js"></script> <script type="text/javascript" src="omnibox.js"></script> <script type="text/javascript" src="rssreader.js"></script> <script type="text/javascript" src="contextmenus.js"></script> </head> <body> </body> </html>
- کدنویسی راحتتر و خلاصهتر برای خواندن RSS
- استفاده اجباری از یک پروکسی به خاطر Content Security Policy و حتی CORS
"content_security_policy": "script-src 'self' https://*.google.com; object-src 'self'"
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
استفاده از این Api در rssreader.js
google.load("feeds", "1"); google.setOnLoadCallback(alarmManager);
function alarmManager() { chrome.storage.local.get(DateContainer.interval,function ( items) { period_time==items[DateContainer.interval]; chrome.alarms.create('RssInterval', {periodInMinutes: period_time}); }); chrome.alarms.onAlarm.addListener(function (alarm) { console.log(alarm); if (alarm.name == 'RssInterval') { var boolposts,boolpostsComments,boolshares,boolsharesComments; chrome.storage.local.get([Variables.posts,Variables.postsComments,Variables.shares,Variables.sharesComments],function ( items) { boolposts=items[Variables.posts]; boolpostsComments=items[Variables.postsComments]; boolshares=items[Variables.shares]; boolsharesComments=items[Variables.sharesComments]; chrome.storage.local.get([DateContainer.posts,DateContainer.postsComments,DateContainer.shares,DateContainer.sharesComments],function ( items) { var Vposts=new Date(items[DateContainer.posts]); var VpostsComments=new Date(items[DateContainer.postsComments]); var Vshares=new Date(items[DateContainer.shares]); var VsharesComments=new Date(items[DateContainer.sharesComments]); if(boolposts){var result=RssReader(Links.postUrl,Vposts,DateContainer.posts,Messages.PostsUpdated);} if(boolpostsComments){var result=RssReader(Links.posts_commentsUrl,VpostsComments,DateContainer.postsComments,Messages.CommentsUpdated); } if(boolshares){var result=RssReader(Links.sharesUrl,Vshares,DateContainer.shares,Messages.SharesUpdated);} if(boolsharesComments){var result=RssReader(Links.shares_CommentsUrl,VsharesComments,DateContainer.sharesComments,Messages.SharesCommentsUpdated);} }); }); } }); }
- آدرس فیدی که قرار است از روی آن بخواند
- آخرین به روزسانی که از سایت داشته متعلق به چه تاریخی است.
- نام کلید ذخیره سازی تاریخ آخرین تغییر سایت که اگر بررسی شد و مشخص شد سایت به روز شده است، تاریخ جدید را روی آن ذخیره کنیم.
- در صورتی که سایت به روز شده باشد نیاز است پیامی را برای کاربر نمایش دهیم که این پیام را در اینجا قرار میدهیم.
function RssReader(URL,lastupdate,datecontainer,Message) { var feed = new google.feeds.Feed(URL); feed.setResultFormat(google.feeds.Feed.XML_FORMAT); feed.load(function (result) { if(result!=null) { var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent; var RssUpdate=new Date(strRssUpdate); if(RssUpdate>lastupdate) { SaveDateAndShowMessage(datecontainer,strRssUpdate,Message) } } }); }
var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent;
function SaveDateAndShowMessage(DateField,DateValue,Message) { var params={ } params[DateField]=DateValue; chrome.storage.local.set( params,function(){ var options={ type: "basic", title: Messages.SiteUpdated, message: Message, iconUrl: "icon.png" } chrome.notifications.create("",options,function(){ chrome.notifications.onClicked.addListener(function(){ chrome.tabs.create({'url': WebLinks.Home}, function(tab) { }); }); }); }); }
"permissions": [ "storage", "tabs", "alarms", "notifications" ]
chrome.notifications.create("",options,function(){ chrome.notifications.onClicked.addListener(function(){ chrome.tabs.create({'url': WebLinks.Home}, function(tab) { });}); });
"web_accessible_resources": [ "icon.png" ]
خوب؛ کار افزونه تمام شده است ولی اجازه دهید این بار امکانات افزونه را بسط دهیم:
"options_page": "popup.html"
جایزگزینی صفحات یا Override Pages
"chrome_url_overrides": { "newtab": "newtab.htm" }
ایجاد یک تب اختصاصی در Developer Tools
"devtools_page": "devtools.htm"
<script src="devtools.js"></script>
chrome.devtools.panels.create( "Dotnettips Updater Tools", "icon.png", "devtoolsui.htm", function(panel) { } );
- One-Time Requests یا درخواستهای تک مرتبهای
- Long-Lived Connections یا اتصالات بلند مدت یا مصر
درخواستهای تک مرتبه ای
window.addEventListener("load", function() { chrome.extension.sendMessage({ type: "dom-loaded", data: { myProperty : "value" } }); }, true);
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { switch(request.type) { case "dom-loaded": alert(request.data.myProperty ); break; } return true; });
var port = chrome.runtime.connect({name: "my-channel"}); port.postMessage({myProperty: "value"}); port.onMessage.addListener(function(msg) { // do some stuff here });
chrome.runtime.onConnect.addListener(function(port) { if(port.name == "my-channel"){ port.onMessage.addListener(function(msg) { // do some stuff here }); } });
نمونه کد
آپلود نهایی کار در Google web store
برای آپلود نهایی کار به google web store که در آن تمامی برنامهها و افزونههای کروم قرار دارند بروید. سمت راست آیکن تنظیمات را بزنید و گزینه developer dashboard را انتخاب کنید تا صفحهی آپلود کار برای شما باز شود. دایرکتوری محتویات اکستنشن را zip کرده و آپلود نمایید. توجه داشته باشید که محتویات و سورس خود را باید آپلود کنید نه فایل crx را. بعد از آپلود موفقیت آمیز، صفحهای ظاهر میشود که از شما آیکن افزونه را در اندازه 128 پیکسل میخواهد بعلاوه توضیحاتی در مورد افزونه، قیمت گذاری که به طور پیش فرض به صورت رایگان تنظیم شده است، لینک وب سایت مرتبط، لینک محل پرسش و پاسخ برای افزونه، اگر لینک یوتیوبی در مورد افزونه دارید، یک شات تصویری از افزونه و همینطور چند تصویر برای اسلایدشو سازی که در همان صفحه استاندارد آنها را توضیح میدهد و در نهایت گزینهی جالبتر هم اینکه اکستنشن شما برای چه مناطقی تهیه شده است که متاسفانه ایران را ندیدم که میتوان همه موارد را انتخاب کرد. به خصوص در مورد ایران که آی پیها هم صحیح نیست، انتخاب ایران چنان تاثیری ندارد و در نهایت گزینهی publish را میزنید که متاسفانه بعد از این صفحه درخواست میکند برای اولین بار باید 5 دلار آمریکا پرداخت شود که برای بسیاری از ما این گزینه ممکن نیست.
سورس پروژه را میتوانید از اینجا ببینید و خود افزونه را از اینجا دریافت کنید.
public class AccessControlService { private DbContext db; public AccessControlService() { db = new DbContext(); } public IEnumerable<Permission> GetUserPermissions(string userId) { var userRoles = this.GetUserRoles(userId); var userPermissions = new List<Permission>(); foreach (var userRole in userRoles) { foreach (var permission in userRole.Permissions) { // prevent duplicates if (!userPermissions.Contains(permission)) userPermissions.Add(permission); } } return userPermissions; } public IEnumerable<Role> GetUserRoles(string userId) { return db.Users.FirstOrDefault(x => x.UserId == userId).Roles.ToList(); } public bool HasPermission(string userId, string area, string control) { var found = false; var userPermissions = this.GetUserPermissions(userId); var permission = userPermissions.FirstOrDefault(x => x.Area == area && x.Control == control); if (permission != null) found = true; return found; } {
[RoutePrefix("َAuth/permissions")] public class PermissionsController : ApiController { private AccessControlService _AccessControlService = null; public PermissionsController() { _AccessControlService = new AccessControlService(); } [Route("GetUserPermissions")] public async Task<IHttpActionResult> GetUserPermissions() { if (!User.Identity.IsAuthenticated) { return Unauthorized(); } return Ok(_AccessControlService.GetPermissions(User.Identity.GetCurrentUserId())); } }
توسعه سرویسها و فرآیندهای سمت وب کلاینت AngularJS
'use strict'; angular.module('App').factory('permissionService', ['$http', '$q', function ($http, $q) { var _getUserPermissions = function () { return $http.get(serviceBaseUrl + '/api/permissions/GetUserPermissions/'); } var _isAuthorize = function (area, control) { return _.some($scope.permissions, { 'area': area, 'control': control }); } return { getUserPermissions: _getUserPermissions, isAuthorize: _isAuthorize }; }]);
$scope.login = function () { authService.login($scope.loginData).then(function (response) { savePermissions(); $location.path('/userPanel'); }, function (err) { $scope.message = err.error_description; }); }; var savePermissions = function () { permissionService.getUserPermissions().then(function (response) { $rootScope.permissions = response.data; }, function (err) { }); } }
App.controller('parentController', ['$rootScope', '$scope', 'authService', 'permissionService', function ($rootScope, $scope, authService, permissionService) { $scope.authentication = authService.authentication; // isAuthorize Method $scope.isAuthorize = permissionService.isAuthorize(); // rest of codes }]);
<div ng-controller="childController"> <div ng-if="isAuthorize('articles', 'edit')" > <!-- the block that we want to not see unauthorize person --> </div> </div>