بهبود SEO در ASP.NET MVC
- قسمت new RedirectResult آنرا در سایت خودتان لاگ کنید، تا مشخص شود چندبار و به چه علتی اجرا میشود. همچنین بررسی کنید آیا جای دیگری در برنامه Redirect دیگری تنظیم شده یا خیر.
مشاهده، ویرایش و ذخیره فایل های microsoft office word در وب
هدف از این قسمت سایت مورد دیگری است.
BulkInsert در EF CodeFirst
پس از نصب Git اطمینان حاصل کنید که NodeJs ، npm و jspm نیز بر روی سیستم شما نصب باشند. در این قسمت گفتهایم که چگونه از این «اطمینان» آگاه شوید.
حال نوبت به ساخت اولین پروژهی MVC ما میرسد. یک پروژه MVC جدید با نام دلخواه خودتان در مسیر دلخواه خودتان ایجاد کنید. با خط فرمان، در ریشهی پروژه دستور زیر را اجرا کنید:
jspm init
بدون هیچ تغییری، به هیچ کدام از سوالات پاسخ ندهید و از دکمهی enter استفاده کنید تا مقادیر پیشفرض اعمال شوند. اگر تصویر زیر را در خروجی مشاهده کردید یعنی تا بدین جای کار به درستی پیش رفتهاید :
حالا نوبت به نصب محتویات Aurelia میباشد. برای این کار دستورات زیر را اجرا کنید :
jspm install aurelia-framework jspm install aurelia-bootstrapper
توجه داشته باشید، اگر دستورات بالا به درستی اجرا و تکمیل شوند، باید پس از پایان هر دستور، پیام زیر را در انتهای خروجی مشاهده کنید:
پس از این کارها، فایل Layout را باز کنید و کدهای آن را به صورت زیر تغییر دهید:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Aurelia - www.dotnettips.info</title> </head> <body aurelia-app> <div> @RenderBody() </div> <script src="~/jspm_packages/system.js"></script> <script src="~/config.js"></script> <script> System.import("aurelia-bootstrapper"); </script> </body> </html>
export class App { }
<template> <h3>www.dotnettips.info</h3> </template>
در بخشهای بعدی در مورد کدهای فوق و همچنین به سایر مباحث دیگر Aurelia میپردازیم.
نکتهی تکمیلی
زمانیکه میخواهید از دستوارت jspm استفاده کنید، باید به ریشهی برنامه مراجعه کنید. حال اگر پوشههای تودرتوی زیادی داشته باشید، این رفت و آمدها زمانبر و خسته کننده خواهند شد. برای حل این مشکل کافیست روی پروژه، راست کلیک کنید و گزینهی Open Folder in File Explorer را انتخاب کنید تا ریشهی پروژه باز شود. حالا کافیست در فضای خالی Windows Explorer کلید Shift را گرفته و راست کلیک کنید. حالا یک آیتم جدید با نام Open command window here ظاهر شده است. کافیست روی آن کلیک کنید. CMD در ریشهی سایت باز خواهد شد.
دانلود پروژه جاری در مخزن گیت
Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyDbContext, Configuration>());
An exception occurred while initializing the database. See the InnerException for details. به وجود میآید و البته دیتابیس در SQL Server ایحاد شده و فقط در زمان ارسال هر گونه کوری به لایه سرویس این مورد پیش میآید .
اما با جایگرین شدن در قسمت Application_Start() از
Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyDbContext, Configuration>());
Database.SetInitializer<MoneyExDbContext>(null);
بخش اول - آشنایی و شروع کار با Svelte
LINQ یک DLS بر مبنای .NET می باشد که برای پرس و جو در منابع داده ای مانند پایگاههای داده ، فایلهای XML و یا لیستی از اشیاء درون حافظه کاربرد دارد.
یکی از بزرگترین مزیتهای آن Syntax آسان و خوانا آن میباشد.
LINQ از 2 نوع نمادگذاری پشتیبانی میکند:
- Inline LINQ یا query expressions :
var result = from product in dbContext.Products where product.Category.Name == "Toys" where product.Price >= 2.50 select product.Name;
- Fluent Syntax :
var result = dbContext.Products .Where(p => p.Category.Name == "Toys" && p.Price >= 250) .Select(p => p.Name);
در پرس و چوهای بالا فیلدهای مورد نیاز در قسمت Select در زمان Compile شناخته شده هستند . اما گاهی ممکن است فیلدهای مورد نیاز در زمان اجرا مشخص شوند.
به عنوان مثال یک گزارش ساز پویا که کاربر مشخص میکند چه ستون هایی در خروجی نمایش داده شوند یا یک جستجوی پیشرفته که ستونهای خروجی به اختیار کاربر در زمان اجرا مشخص میشوند.
این مدل را در نظر داشته باشید :
public class Student { public int Id { get; set; } public string Name { get; set; } public string Field1 { get; set; } public string Field2 { get; set; } public string Field3 { get; set; } public static IEnumerable<Student> GetStudentSource() { for (int i = 0; i < 10; i++) { yield return new Student { Id = i, Name = "Name " + i, Field1 = "Field1 " + i, Field2 = "Field2 " + i, Field3 = "Field3 " + i }; } } }
ستونهای کلاس Student را در رابط کاربری برنامه جهت انتخاب به کاربر نمایش میدهیم. سپس کاربر یک یا چند ستون را انتخاب میکند که قسمت Select کوئری برنامه باید بر اساس فیلدهای مورد نظر کاربر مشخص شود.
یکی از روش هایی که میتوان از آن بهره برد استفاده از کتاب خانه Dynamic LINQ معرفی شده در اینجا می باشد.
این کتابخانه جهت سهولت در نصب به کمک NuGet در این آدرس قرار دارد.
فرض بر این است که فیلدهای انتخاب شده توسط کاربر با "," از یکدیگر جدا شده اند.
public class Program { private static void Main(string[] args) { System.Console.WriteLine("Specify the desired fields : "); string fields = System.Console.ReadLine(); IEnumerable<Student> students = Student.GetStudentSource(); IQueryable output = students.AsQueryable().Select(string.Format("new({0})", fields)); foreach (object item in output) { System.Console.WriteLine(item); } System.Console.ReadKey(); } }
همانطور که در عکس ذیل مشاهده میکنید پس از اجرای برنامه ، فیلدهای انتخاب شده توسط کاربر از منبع دادهی دریافت شده و در خروجی نمایش داده شده اند.
این روش مزایا و معایب خودش را دارد ، به عنوان مثال خروجی یک لیست از شیء Student نیست یا این Select فقط برای روی یک شیء IQueryable قابل انجام است.
روش دیگری که میتوان از آن بهره جست استفاده از یک متد کمکی جهت تولید پویای عبارت Lambda ورودی Select می باشد :
public class SelectBuilder <T> { public static Func<T, T> CreateNewStatement(string fields) { // input parameter "o" var xParameter = Expression.Parameter(typeof(T), "o"); // new statement "new T()" var xNew = Expression.New(typeof(T)); // create initializers var bindings = fields.Split(',').Select(o => o.Trim()) .Select(o => { // property "Field1" var property = typeof(T).GetProperty(o); // original value "o.Field1" var xOriginal = Expression.Property(xParameter, property); // set value "Field1 = o.Field1" return Expression.Bind(property, xOriginal); } ).ToList(); // initialization "new T { Field1 = o.Field1, Field2 = o.Field2 }" var xInit = Expression.MemberInit(xNew, bindings); // expression "o => new T { Field1 = o.Field1, Field2 = o.Field2 }" var lambda = Expression.Lambda<Func<T, T>>(xInit, xParameter); // compile to Func<T, T> return lambda.Compile(); } }
IEnumerable<Student> result = students.Select(SelectBuilder<Student>.CreateNewStatement("Field1, Field2")).ToList(); foreach (Student student in result) { System.Console.WriteLine(student.Field1); }
ابتدا فیلدهای انتخابی کاربر که با "," جدا شده اند به ورودی پاس داده میشود سپس یک statement خالی ایجاد میشود :
o=>new Student()
var property = typeof(T).GetProperty(o);
برپایی پیشنیازها
در اینجا نیز از همان برنامهای که در قسمت 30، برای بررسی مثالهای React hooks ایجاد کردیم، استفاده خواهیم کرد. فقط در آن، کتابخانهی Axios را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
در ادامه میخواهیم در برنامهی React خود، لیست مطالب برنامهی backend را از سرور دریافت کرده و نمایش دهیم. همچنین یک search box را به همراه دکمههای search و clear نیز به آن اضافه کنیم.
دریافت اطلاعات اولیه از سرور، درون useEffect Hook
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api آن، ایجاد میکنیم:
{ "apiUrl": "https://localhost:5001/api" }
import React from "react"; import {apiUrl} from "../../config.json"; export default function App() { return <></>; }
import App from "./components/part03/Search";
import axios from "axios"; import React, { useEffect, useState } from "react"; import { apiUrl } from "../../config.json"; export default function App() { useEffect(() => { axios .get(apiUrl + "/posts/search?query=") .then(response => console.log(response.data)); }); return <></>; }
اکنون میخواهیم این اطلاعات دریافتی را در برنامهی خود نیز نمایش دهیم. به همین جهت نیاز است تا response.data را درون state کامپوننت جاری قرار داده و در حین رندر کامپوننت، با تشکیل حلقهای بر روی آن، اطلاعات نهایی را نمایش دهیم. بنابراین نیاز به useState Hook خواهیم داشت که ابتدا آنرا import کرده و سپس آنرا تعریف و در قسمت then، فراخوانی میکنیم:
import axios from "axios"; import React, { useEffect, useState } from "react"; import { apiUrl } from "../../config.json"; export default function App() { const [results, setResults] = useState([]); useEffect(() => { axios.get(apiUrl + "/posts/search?query=").then(response => { console.log(response.data); setResults(response.data); }); });
همانطور که مشاهده میکنید، یک حلقهی بی پایان در اینجا رخ دادهاست! برای پایان آن، مجبور خواهیم شد ابتدا کنسول اجرایی برنامهی React را به صورت دستی خاتمه داده و سپس مرورگر را نیز refresh کنیم تا این حلقه، خاتمه پیدا کند.
علت این مشکل را در قسمت 30 بررسی کردیم؛ effect method تابع useEffect (همان متد در برگیرندهی قطعه کدهای axios.get در اینجا)، پس از هربار رندر کامپوننت، یکبار دیگر نیز اجرا میشود. یعنی این متد، هر دو حالت componentDidMount و componentDidUpdate کامپوننتهای کلاسی را با هم پوشش میدهد و چون در اینجا setState را با فراخوانی متد setResults داریم، یعنی درخواست رندر مجدد کامپوننت انجام شدهاست و پس از آن، مجددا effect method فراخوانی میشود و ... این حلقه هیچگاه خاتمه نخواهد یافت. به همین جهت مرورگر و برنامه، هر دو با هم هنگ میکنند!
در این برنامه فعلا میخواهیم که فقط در حالت componentDidMount، کار درخواست اطلاعات از backend صورت گیرد. به همین جهت پارامتر دوم متد useEffect را با یک آرایهی خالی مقدار دهی میکنیم:
useEffect(() => { // ... }, []);
//... export default function App() { // ... return ( <> <table className="table"> <thead> <tr> <th>Title</th> </tr> </thead> <tbody> {results.map(post => ( <tr key={post.id}> <td>{post.title}</td> </tr> ))} </tbody> </table> </> ); }
استفاده ازAsync/Await برای دریافت اطلاعات، درون یک useEffect Hook
اکنون میخواهیم درون effect method یک useEffect Hook، روش قدیمی استفادهی از callbackها و متد then را برای دریافت اطلاعات، با روش جدیدتر async/await که در قسمت 23 آنرا بیشتر بررسی کردیم، جایگزین کنیم.
useEffect(async () => { const { data } = await axios.get(apiUrl + "/posts/search?query="); console.log(data); setResults(data); }, []);
Warning: An effect function must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => ...) or returned a Promise.
برای رفع این مشکل، روش توصیه شده، ایجاد یک تابع مجزای async و سپس فراخوانی آن درون effect function است:
useEffect(() => { getResults(); }, []); const getResults = async () => { const { data } = await axios.get(apiUrl + "/posts/search?query="); console.log(data); setResults(data); };
پیاده سازی componentDidUpdate با یک useEffect Hook، جهت انجام جستجوهای پویا
تا اینجا با اضافه کردن پارامتر دومی به متد useEffect، رویداد componentDidUpdate آنرا از کار انداختیم، تا برنامه با هربار فراخوانی setState و اجرای مجدد effect function، در یک حلقهی بینهایت وارد نشود. اکنون این سؤال مطرح میشود که اگر یک textbox را برای جستجوی در عناوین نمایش داده شده، در بالای جدول آن قرار دهیم، نیاز است با هربار تغییر ورودی آن، کار فراخوانی مجدد effect function صورت گیرد، تا بتوان نتایج جدیدتری را از سرور دریافت و به کاربر نشان داد؛ این مشکل را چگونه باید حل کرد؟
برای دریافت عبارت وارد شدهی توسط کاربر و جستجو بر اساس آن، ابتدا متغیر state و متد تنظیم آنرا با استفاده از useState Hook و یک مقدار اولیهی دلخواه تنظیم میکنیم:
export default function App() { // ... const [query, setQuery] = useState("Title");
<input type="text" name="query" className="form-control my-3" placeholder="Search..." onChange={event => setQuery(event.target.value)} value={query} />
اکنون که متغیر query دارای مقدار شدهاست، میتوان از آن در متد axios.get، به نحو زیر و با ارسال یک کوئری استرینگ به سمت سرور، استفاده کرد:
const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` );
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، با تایپ در textbox جستجو، تغییری در نتایج حاصل نمیشود؛ چون effect function تعریف شده که سبب اجرای مجدد axios.get میشود، طوری تنظیم شدهاست که فقط یکبار، آنهم پس از رندر اولیهی کامپوننت، اجرا شود. برای رفع این مشکل، با مقدار دهی آرایهای که به عنوان پارامتر دوم متد useEffect تعریف شده، میتوان اجرای مجدد effect function آنرا وابستهی به تغییرات متغیر query در state کامپوننت کرد:
useEffect(() => { getResults(); }, [query]);
دریافت اطلاعات جستجو، تنها با ارسال اطلاعات یک فرم به سمت سرور
تا اینجا کاربر با هر حرفی که درون textbox جستجو وارد میکند، یک کوئری، به سمت سرور ارسال خواهد شد. برای کاهش آن میتوان یک دکمهی جستجو را در کنار این textbox قرار داد تا تنها پس از کلیک بر روی آن، این جستجو صورت گیرد.
برای پیاده سازی این قابلیت، ابتدا وابستگی به query را از متد useEffect حذف میکنیم، تا دیگر با تغییر اطلاعات textbox، متد callback آن اجرا نشود (پارامتر دوم آنرا مجددا به یک آرایهی خالی تنظیم میکنیم). سپس یک دکمه را که از نوع button است و رویداد onClick آن به getResults اشاره میکند، در بالای جدول نتایج مطالب، قرار میدهیم:
<button className="btn btn-primary" type="button" onClick={getResults} > Search </button>
<form onSubmit={handleSearch}> <div className="input-group my-3"> <label htmlFor="query" className="form-control-label sr-only"></label> <input type="text" id="query" name="query" className="form-control" placeholder="Search ..." onChange={event => setQuery(event.target.value)} value={query} /> <div className="input-group-append"> <button className="btn btn-primary" type="submit"> Search </button> </div> </div> </form>
const handleSearch = event => { event.preventDefault(); getResults(); };
افزودن قابلیت پاک کردن textbox جستجو و معرفی useRef Hook
در ادامه میخواهیم یک دکمهی جدید را در کنار دکمهی Search، اضافه کنیم تا با کلیک کاربر بر روی آن، نه فقط محتوای وارد شدهی در textbox پاک شود، بلکه focus نیز به آن منتقل گردد. برای پاک کردن textbox، فقط کافی است متد setQuery را با یک رشتهی خالی ارسالی به آن فراخوانی کنیم. اما برای انتقال focus به textbox، نیاز به داشتن ارجاع مستقیمی به آن المان وجود دارد که با مفهوم آن در قسمت 18 آشنا شدیم: «برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت ایجاد کرده و آنرا با React.RefObject مقدار دهی اولیه کرده و سپس ویژگی ref المان مدنظر را به این RefObject تنظیم میکنیم». برای انجام یک چنین کاری در اینجا، Hook ویژهای به نام useRef معرفی شدهاست. بنابراین برای پیاده سازی این نیازمندیها، ابتدا دکمهی Clear را در کنار دکمهی Search قرار میدهیم:
<button type="button" onClick={handleClearSearch} className="btn btn-info" > Clear </button>
import React, { useEffect, useRef, useState } from "react"; // ... export default function App() { // ... const searchInputRef = useRef(); const handleClearSearch = () => { setQuery(""); searchInputRef.current.focus(); };
البته این searchInputRef برای اینکه دقیقا به textbox تعریف شده اشاره کند، باید آنرا به ویژگی ref المان، انتساب داد:
<input type="text" id="query" name="query" className="form-control" placeholder="Search ..." onChange={event => setQuery(event.target.value)} value={query} ref={searchInputRef} />
نمایش «لطفا منتظر بمانید» در حین دریافت اطلاعات از سرور
البته در اینجا با هر بار کلیک بر روی دکمهی جستجو، نتیجهی نهایی به سرعت نمایش داده میشود؛ اما اگر سرعت اتصال کاربر کمتر باشد، با یک وقفه این امر رخ میدهد. به همین جهت بهتر است یک پیام «لطفا منتظر بمانید» را در این حین به او نمایش دهیم. به همین جهت در ابتدا state مرتبطی را به کامپوننت اضافه میکنیم:
const [loading, setLoading] = useState(false);
const getResults = async () => { setLoading(true); const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` ); console.log(data); setResults(data); setLoading(false); };
{loading ? ( <div className="alert alert-info">Loading results...</div> ) : ( <table className="table"> <thead> <tr> <th>Title</th> </tr> </thead> <tbody> {results.map(post => ( <tr key={post.id}> <td>{post.title}</td> </tr> ))} </tbody> </table> )}
برای آزمایش آن میتوان سرعت اتصال را در برگهی شبکهی ابزارهای توسعه دهندگان مرورگر، تغییر داد:
مدیریت خطاها در حین اعمال async
آخرین امکانی را که به این مطلب اضافه خواهیم کرد، مدیریت خطاهای اعمال async است که با try/catch صورت میگیرد:
// ... export default function App() { // ... const [error, setError] = useState(null); // ... const getResults = async () => { setLoading(true); try { const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` ); console.log(data); setResults(data); } catch (err) { setError(err); } setLoading(false); };
{error && <div className="alert alert-warning">{error.message}</div>}
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-03-frontend.zip و sample-30-part-03-backend.zip