- Basic Spatial Data with SQL Server and Entity Framework 5.0
- Using spatial data types with Entity Framework 5.0
- Using Spatial Data with SQL Server 2012, Entity Framework 5 and ASP.NET MVC
- Spatial Data Type Support in Entity Framework 5
- Using Spatial Data in ASP.Net MVC with SQL Server 2012, Entity Framework 5 and Infragistics jQuery Map
- Spatial Data and the Entity Framework
- Spatial data and Entity Framework - from real world usage
- Entity Framework Spatial Part 1: Loading data
- Spatial Data type support in Entity Framework 5.0
- Creating a Geometry Point in Entity Framework
- Spatial - Code First
- Spatial Types in the Entity Framework
- How To: Using spatial data with Entity Framework and Connector/Net
sudo apt-get update -y sudo apt-get upgrade -y
sudo java -version
sudo add-apt-repository -y ppa:webupd8team/java
gpg: keyring `/tmp/tmpkjrm4mnm/secring.gpg' created gpg: keyring `/tmp/tmpkjrm4mnm/pubring.gpg' created gpg: requesting key EEA14886 from hkp server keyserver.ubuntu.com gpg: /tmp/tmpkjrm4mnm/trustdb.gpg: trustdb created gpg: key EEA14886: public key "Launchpad VLC" imported gpg: no ultimately trusted keys found gpg: Total number processed: 1 gpg: imported: 1 (RSA: 1) OK
sudo apt-get update
sudo apt-get install oracle-java8-installer -y
sudo java -version
java version "1.8.0_66" Java(TM) SE Runtime Environment (build 1.8.0_66-b17) Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)
پس از دریافت Apache Kafka، از طریق یک terminal جدید وارد پوشه Downloads شوید. سپس فایل مورد نظر را از حالت فشرده خارج کنید و وارد پوشه اصلی آن شوید. برای این کار از دستورات زیر استفاده کنید:
tar -xzf kafka_2.11-1.0.0.tgz cd kafka_2.11-1.0.0
سپس با استفاده از دستور "ls " لیست تمامی فایلها و فولدرهای موجود در فولدر kafka_2.11-1.0.0 و را نمایش دهید:
در لیست نمایش داده شده دو فولد اصلی bin و config وجود دارند که به ترتیب فایلهای اجرایی و کانفیگهای پیشفرض و مورد نیاز، در آنها قرار دارند.
اجرای Apache ZooKeeper:
همانطور که در بخش قبل توضیح داده شد، Apache Kafka هیچ Stateی را در خود ذخیره و مدیریت نمیکند و اصطلاحا Stateless میباشد و مدیریت تمامی این Stateها را بر عهده Apache ZooKeeper قرار میدهد. بنابراین قبل از اینکه بخواهیم Kafka را اجرا کنیم، ابتدا باید Apache ZooKeeper را اجرا کنیم. برای اجرای Apache ZooKeeper در terminalی که قبلا باز کردهاید، دستور زیر را اجرا کنید:
bin/zookeeper-server-start.sh config/zookeeper.properties
با اجرای فایل zookeeper-server-start.sh و ارسال کانفیگ پیشفرضش در فولدر config با نام zookeeper.properties، قسمت مدیریت کننده Stateهای Kafka اجرا میشود.
اجرای Apache Kafka:
پس از اجرای Apache ZooKeeper، باید یک terminal جدید را باز کنید و با استفاده از دستور زیر kafka-server را با استفاده از کانفیگ پیشفرضش اجرا کنید:
bin/kafka-server-start.sh config/server.properties
به همین راحتی Kafka Server اجرا شده و آمادهی استفاده است. برای ادامه و نمایش دادن سایر قابلیتهای Kafka باید یک Topic جدید را ایجاد کنیم.
ایجاد Topic:
همانطور که میدانید تمامی پیامهای شما در Partitionهای Topic ذخیره میشوند؛ پس قبل از اینکه بخواهیم توسط یک Producer پیامی را ارسال کنیم یا اینکه بخواهیم توسط Consumer پیامی را دریافت کنیم، ابتدا باید Topic مربوطه را ایجاد کنیم. بهترین و عمومیترین مثال برای نمایش قابلیتهای Publish-Subscribe در Kafka، مثال چت بین کاربران است که در آن یک کاربر به عنوان Producer عمل میکند و پیامهایی را برای سایر کاربران که نقش Consumer را دارند ارسال میکند. kafka-topics
.sh
در Kafka ابزاریست برای مدیریت Topic ها که با استفاده از آن میتوانید Topicهای مورد نیاز را تعریف، ویرایش و یا حذف کنید.
یک terminal جدید را آغاز و با استفاده از دستور زیر یک Topic را با نام userChat ایجاد کنید:
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic userChat
با این دستور یک Topic در Kafka با نام userChat ایجاد میشود و مشخصات آن (منظور stateهای مرتبط با آن است)، در zookeeper نیز ثبت میشود.
توضیحات Optionها و آرگومانهایی که در دستور ایجاد Topic استفاده شدند به شرح زیر است:
create-- :
مشخص کننده این است که شما میخواهید یک Topic جدید را ایجاد کنید.
zookeeper localhost:2181--:
مشخص کننده آدرس سرور zookeeper است (سرور zookeeper بصورت پیشفرض از پورت 2181 استفاده میکند).
replication-factor 1--:
مشخص کننده این است که تنها یک کپی از این Topic در سرور ایجاد شود. البته در این مثال به دلیل اینکه تنها یک سرور از Kafka را اجرا کردهایم در صورتیکه این مقدار را بیش از عدد 1 در نظر بگیریم، با خطا مواجه میشویم.
partitions 1--
تعداد Partitionهای این Topic را مشخص میکند. مقدار Partition در حالتیکه حتی یک سرور را نیز اجرا کردهایم، میتواند بیش از 1 باشد؛ اما در این حالت Primary Broker برای تمام Partitionهای همین سرور در نظر گرفته میشود.
topic userChat--
نام Topic را مشخص میکند.
پس از اینکه Topic مورد نظر ایجاد شد، با استفاده از دستور زیر میتوانیم لیست Topicهای ایجاد شده را مشاهده کنیم:
bin/kafka-topics.sh --list --zookeeper localhost:2181
توضیحات Optionها و آرگومانهایی که در دستور نمایش لیست Topicها استفاده شدند به شرح زیر است:
list--:
شما میخواهید لیست topicها را مشاهده کنید.
zookeeper localhost:2181--:
همانند دستور ایجاد، مشخص کننده سرور zookeeper میباشد.
تا اینجا برای ارسال و دریافت پیام در بین Applicationهای مختلف همه چیز آمادهاست. البته همانطور که در بخشهای بعدی این سری مقالات میبینیم، در عمل Producerها و Consumerها زیرسیستمهایی هستند که خود ما با استفاده از هر زبان برنامه نویسی پیاده سازی میکنیم. اما در این قسمت برای نمایش و تکمیل مثالمان، از ابزارهایی که خود Kafka در اختیار ما قرار داده استفاده میکنیم.
اجرای یک Producer و ارسال پیام به Kafka:
kafka-console-producer.sh ابزاریست که با استفاده از آن میتوانید به راحتی یک Producer را ایجاد کنید. در terminalی که قبلا برای مدیریت Topicها باز کردهاید با استفاده از دستور زیر، Producer را اجرا میکنیم:
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic userChat
توضیحات Optionها و آرگومانهایی که در دستور ایجاد Producer استفاده شدند به شرح زیر است:
broker-list localhost:9092--:
نشان دهنده لیست Brokerها میباشد و در صورتیکه تعداد آنها بیش از یک باشد، میتوان آنها را با "," از هم جدا کرد (پورت پیشفرض Kafka server برای دریافت دستورات، 9092 میباشد).
topic userChat--:
Topicی از Kafka server را مشخص میکند که این Producer میخواهد پیامهای خود را برایش ارسال کند.
پس از اجرای دستور فوق، با تایپ کردن و زدن کلید Enter، پیام مورد نظر به Broker موردنظر یعنی localhost:9092 ارسال و در تنها Partition تاپیک userChat ذخیره میشود.
اجرای یک Consumer و دریافت پیامهای موجود در Topic مورد نظر:
kafka-console-consumer.sh ابزاریست که خود Kafka در اختیار ما قرار داده است و با استفاده از آن میتوانیم به راحتی یک Consumer برای userChat ،Topic ایجاد کنیم.
در یک terminal جدید با استفاده از دستور زیر یک Consumer را ایجاد کنید که userChat ،Topic را Subscribe میکند:
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic userChat --from-beginning
توضیحات Optionها و آرگومانهایی که در دستور ایجاد یک Consumer استفاده شدند به شرح زیر است:
bootstrap-server localhost:9092-- :
مشخص کننده Kafka serverی است که میخواهیم از طریق آن به تمامی اعضای Cluster دسترسی داشته باشیم. در صورتیکه بیش از یک سرور را بخواهیم وارد کنیم، باید آنها را با "," از هم جدا کنیم.
topic userChat --:
مشخص کننده Topicی است که این Consumer میخواهد روی آن Subscribe کند.
from-beginning--:
مشخص کننده این است که Consumer ایجاد شده میخواهد تمامی پیامهای موجود در userChat ، Topic را از اولین offset تا آخرین offset دریافت کند.
پس از اجرای کد فوق مشاهده میکنید که پیامهایی که قبلا Producer در تاپیک userChat ثبت کردهاست، برای این Consumer ارسال میشوند.
از اینجا به بعد هر لحظه که در terminal ارسال کننده یا Producer پیامی تایپ کنید و کلید Enter را بزنید، بلافاصله پیام مورد نظر برای Consumer ارسال میشود و در terminal آن نمایش داده میشود. حتی میتوانیم چندین Consumer را در terminal های مختلفی اجرا کنیم. در این صورت با ارسال هر پیام از طرف Producer، تمامی Consumerها آن را نمایش میدهند.
با استفاده از سادگی راه اندازی و قابلیتهای بسیار زیادی که Apache Kafka در مدیریت جریان دادهای بین سیستمهایمان به ما میدهد، میتوانیم به سادگی در سیستمهایمان قابلیتهای مقیاس پذیری افقی، تحمل در برابر خطا، در دسترس بودن، کارآیی بالا و سادگی مدیریت ارتباطات بخشهای مختلف را اضافه کنیم. در بخشهای بعدی به نحوه ایجاد یک Cluster و اینکه چگونه میتوان از این بستر در Net. استفاده کرد، میپردازیم.
چند روزی هست که به دلیل قطعی کابل، فایلهای فلش سایتها از کار افتاده!
فایلهای فلش را بر اساس mime type آنها فیلتر کردهاند یعنی هر چه application/x-shockwave-flash که از طرف وب سرور شما سرو شود فیلتر میشود.
یک راه حل این است که پسوند تمام فایلهای فلش را تغییر داد تا وب سرور دیگر این mime type را از خودش بروز ندهد (در یک وب سرور هر mime type دقیقا به یک یا چند نوع پسوند فایل map شده). این مورد نیاز به اصلاح تمام صفحات سایت نیز خواهد داشت (علاوه بر تغییر پسوند فایلهای موجود). خوشبختانه فلش پلیر کاری به mime type یا حتی پسوند فایل، جهت نمایش آن ندارد.
راه حل سادهتر (بدون نیاز به تغییری در فایلها) هم این است که کمی تغییرات وب سرور خود را تغییر داد:
در IIS6 :
در IIS7 :
کلا mime type موجود را به برای مثال image/png تغییر دهید تا قسمتهای از کار افتاده سایت مجددا برقرار شود و آبروی کاری رفته مجددا به جوی بازگردد.
آموزش Knockout.Js #2
بله امکان پذیر است. باید از المانهای تودرتو استفاده کنیم. به این صورت که المان ریشه با استفاده از with به model مربوطه مقید میشود و المانهای داخلی به خواص مدل bind میشوند. برای مثال:
<div data-bind="text: teacher"> </div>//مقید سازی به مدل اول <p data-bind="with: student">//مقید سازی المان ریشه به مدل دوم Name: <span data-bind="text: name"> </span>,//مقید سازی خواص به المانهای داخلی Family: <span data-bind="text: family"> </span> </p> <script type="text/javascript"> ko.applyBindings({ teacher: "myTeacher", student: { name: "Masoud", family: "Pakdel" } }); </script>
همچنین یک Interface را نیز به نام IResourceManager تعریف میکنیم:
public interface IResourceManager { IStringLocalizer Localizer { get; } }
سپس دو کلاس دیگر را ایجاد کرده و آنها را از این اینترفیس مشتق میکنیم. در این اینترفیس، یک خصوصیت Localizer از نوع IStringLocalizer وجود دارد که باید در این کلاسها پیاده سازی شود. این کار را به کمک IStringLocalizerFactory انجام داده و مشخص میکنیم هر کلاس، از کدام ریسورس استفاده کند:
public class SchoolResourceManager : IResourceManager { public SchoolResourceManager (IStringLocalizerFactory factory) { Localizer = factory.Create(typeof(SchoolResource)); } public IStringLocalizer Localizer { get; } }
public class ArtSchoolResourceManager : IResourceManager { public ArtSchoolResourceManager(IStringLocalizerFactory factory) { Localizer = factory.Create(typeof(ArtSchoolResource)); } public IStringLocalizer Localizer { get; } }
سپس در فایل appsetting.json پروژه، یک کلید را تعریف میکنیم تا مشخص کند، نوع application، از نوع مدرسه هست یا از نوع هنرستان:
{ "ConnectionStrings": { "str": "..." }, "Type": "ArtSchool" }
در متد ConfigureServices در فایل startup تعیین میکنیم هنگام شروع به کار برنامه، با توجه به مقدار درون appsetting، کدام سرویس (manager) تزریق شود:
public void ConfigureServices(IServiceCollection services) { services.AddLocalization(); services.AddSingleton<SchoolResourceManager>(); services.AddSingleton<ArtSchoolResourceManager>(); services.AddSingleton<IResourceManager>(cnf => { var type = Configuration.GetSection("Type").Value; if (type == "ArtSchool") return cnf.GetServices<ArtSchoolResourceManager>().FirstOrDefault(); return cnf.GetServices<SchoolResourceManager>().FirstOrDefault(); }); }
خوب حالا با خیال راحت برنامه را توسعه میدهیم و در viewها و Controllerها از همان کلیدهای School و Student استفاده میکنیم:
: View
@inject IResourceManager ResourceManager <h2>@ResourceManager.Localizer["Student"] : </h2>
:Controller
public class TestController : Controller { private readonly IResourceManager _resourceManager; public TestController (IResourceManager resourceManager) { _resourceManager = resourceManager; } }
اصول کلی طراحی یک اعتبارسنج ساده
در قسمت قبل، تمام اطلاعات فرم لاگین را درون شیء account خاصیت state قرار دادیم. در اینجا نیز شبیه به چنین شیءای را برای ذخیره سازی خطاهای اعتبارسنجی فیلدهای فرم، تعریف میکنیم:
class LoginForm extends Component { state = { account: { username: "", password: "" }, errors: { username: "Username is required" } };
البته در ابتدای کار، خاصیت errors را با یک شیء خالی ({}) مقدار دهی میکنیم و سپس در متد مدیریت ارسال فرم به سرور:
validate = () => { return { username: "Username is required." }; }; handleSubmit = e => { e.preventDefault(); const errors = this.validate(); console.log("Validation errors", errors); this.setState({ errors }); if (errors) { return; } // call the server console.log("Submitted!"); };
- اگر خطایی وجود داشت، به مرحلهی بعد که ارسال فرم به سمت سرور میباشد، نخواهیم رسید و کار را با یک return، خاتمه میدهیم.
- علت فراخوانی متد setState در اینجا، درخواست رندر مجدد فرم، با توجه به خطاهای اعتبارسنجی ممکنی است که به خاصیت errors، اضافه یا به روز رسانی کردهایم.
- نمونهای از خروجی متد validate را نیز در اینجا مشاهده میکنید که تشکیل شدهاست از یک شیء، که هر خاصیت آن، به نام یک فیلد موجود در فرم، اشاره میکند.
پیاده سازی یک اعتبارسنج ساده
در اینجا یک نمونه پیاده سازی ساده و ابتدایی منطق اعتبارسنجی فیلدهای فرم را ملاحظه میکنید:
validate = () => { const { account } = this.state; const errors = {}; if (account.username.trim() === "") { errors.username = "Username is required."; } if (account.password.trim() === "") { errors.password = "Password is required."; } return Object.keys(errors).length === 0 ? null : errors; };
- سپس یک شیء خالی error را تعریف کردهایم.
- در ادامه با توجه به اینکه مقادیر المانهای فرم در state وجود دارند، خالی بودن آنها را بررسی میکنیم. اگر خالی بودند، یک خاصیت جدید را با همان نام المان مورد بررسی، به شیء errors اضافه کرده و پیام خطایی را درج میکنیم.
- در نهایت این شیء errors و یا نال را (در صورت عدم وجود خطایی) بازگشت میدهیم.
برای آزمایش آن، پس از اجرای برنامه، یکبار بدون وارد کردن اطلاعاتی، بر روی دکمهی Login کلیک کنید؛ یکبار هم با وارد کردن اطلاعاتی در فیلدهای مختلف. در این بین کنسول توسعه دهندگان مرورگر را نیز جهت مشاهدهی شیءهای error لاگ شده، بررسی نمائید.
نمایش خطاهای اعتبارسنجی فیلدهای فرم
در قسمت قبل، کامپوننت جدید src\components\common\input.jsx را جهت کاهش کدهای تکراری تعاریف المانهای ورودی، ایجاد کردیم. در اینجا نیز میتوان کار نمایش خطاهای اعتبارسنجی را قرار داد:
import React from "react"; const Input = ({ name, label, value, error, onChange }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input value={value} onChange={onChange} id={name} name={name} type="text" className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
- سپس با توجه به نکتهی «رندر شرطی عناصر در کامپوننتهای React» در قسمت 5، اگر error مقداری داشته باشد و به true تفسیر شود، آنگاه به صورت خودکار، div ای که دارای کلاسهای بوت استرپی اخطار است به همراه متن خطا، رندر خواهد شد؛ در غیراینصورت هیچ div ای به صفحه اضافه نمیشود.
- اکنون متد رندر کامپوننت فرم لاگین را به صورت زیر تکمیل میکنیم:
render() { const { account, errors } = this.state; return ( <form onSubmit={this.handleSubmit}> <Input name="username" label="Username" value={account.username} onChange={this.handleChange} error={errors.username} /> <Input name="password" label="Password" value={account.password} onChange={this.handleChange} error={errors.password} /> <button className="btn btn-primary">Login</button> </form> ); }
تا اینجا اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، با کلیک بر روی دکمهی Login، خطاهای اعتبارسنجی به صورت زیر ظاهر میشوند:
در این حالت اگر هر دو فیلد را تکمیل کرده و بر روی دکمهی لاگین کلیک کنیم، به خطای زیر در کنسول توسعه دهندگان مرورگر میرسیم:
loginForm.jsx:55 Uncaught TypeError: Cannot read property 'username' of null at LoginForm.render (loginForm.jsx:55)
this.setState({ errors: errors || {} });
اعتبارسنجی فیلدهای یک فرم در حین ورود اطلاعات در آنها
تا اینجا نحوهی اعتبارسنجی فیلدهای ورودی را در حین submit بررسی کردیم. شبیه به همین روش را به حالت onChange و متد handleChange فرم لاگین که در قسمت قبل تکمیل کردیم نیز میتوان اعمال کرد:
handleChange = ({ currentTarget: input }) => { const errors = { ...this.state.errors }; //cloning an object const errorMessage = this.validateProperty(input); if (errorMessage) { errors[input.name] = errorMessage; } else { delete errors[input.name]; } const account = { ...this.state.account }; //cloning an object account[input.name] = input.value; this.setState({ account, errors }); };
- سپس اینبار فقط نیاز داریم اعتبار اطلاعات ورودی یک فیلد را بررسی کنیم و متد validate فعلی، فیلدهای کل فرم را با هم تعیین اعتبار میکند. به همین جهت متد جدید validateProperty را به صورت زیر تعریف میکنیم. اگر این متد خروجی داشت، خاصیت متناظر با آنرا در شیء errors به روز رسانی میکنیم؛ در غیراینصورت این خاصیت را از شیء errors حذف میکنیم تا پیام اشتباهی را نمایش ندهد. در نهایت توسط متد setState، مقدار خاصیت errors را با شیء errors جاری به روز رسانی میکنیم:
validateProperty = ({ name, value }) => { if (name === "username") { if (value.trim() === "") { return "Username is required."; } // ... } if (name === "password") { if (value.trim() === "") { return "Password is required."; } // ... } };
پس از ذخیره سازی این تغییرات، برای آزمایش آن، یکبار حرف a را بجای username وارد کنید و سپس آنرا حذف کنید. بلافاصله پیام خطای مرتبطی نمایش داده خواهد شد و اگر مجددا عبارتی را وارد کنیم، این پیام محو میشود که معادل قسمت delete در کدهای فوق است.
معرفی Joi
تا اینجا، هدف نمایش ساختار یک اعتبارسنج ساده بود. این روش مقیاس پذیر نیست و در ادامه آنرا با یک کتابخانهی اعتبارسنجی بسیار پیشرفته به نام Joi، جایگزین خواهیم کرد که نمونه مثالهای آنرا در اینجا میتوانید مشاهده کنید:
const Joi = require('@hapi/joi'); const schema = Joi.object({ username: Joi.string().alphanum().min(3).max(30).required(), password: Joi.string().pattern(/^[a-zA-Z0-9]{3,30}$/), email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } }) }); schema.validate({ username: 'abc', birth_year: 1994 });
برای نصب آن، پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install @hapi/joi --save > npm i --save-dev @types/hapi__joi
سپس به کامپوننت فرم لاگین مراجعه کرده و در ابتدای آن، Joi را import میکنیم:
import Joi from "@hapi/joi";
schema = { username: Joi.string() .required() .label("Username"), password: Joi.string() .required() .label("Password") };
سپس ابتدای متد validate قبلی را به صورت زیر بازنویسی میکنیم:
validate = () => { const { account } = this.state; const validationResult = Joi.object(this.schema).validate(account, { abortEarly: false }); console.log("validationResult", validationResult);
اکنون اگر برنامه را اجرا کرده و بدون ورود اطلاعاتی، بر روی دکمهی لاگین کلیک کنیم، خروجی زیر در کنسول توسعه دهندگان مرورگر ظاهر میشود:
همانطور که مشاهده میکنید، خروجی Joi، یک شیء است که اگر دارای خاصیت error بود، یعنی خطای اعتبارسنجی رخدادهاست. سپس باید خاصیت آرایهای details این شیء error را جهت یافتن خواص مشکل دار بررسی کرد. هر خاصیت در اینجا با path مشخص میشود. بنابراین قدم بعدی، تبدیل این ساختار، به ساختار شیء errors موجود در state کامپوننت جاری است تا مابقی برنامه بتواند بدون تغییری از آن استفاده کند.
نگاشت شیء دریافتی از Joi، به شیء errors موجود در state کامپوننت لاگین
خاصیت error شیء دریافتی از متد validate کتابخانهی Joi، تنها زمانی ظاهر میشود که خطایی وجود داشته باشد. همچنین خاصیت details آن نیز آرایهای از اشیاء با خواص message و path است. این path نیز یک آرایه است که اولین المان آن، نام خاصیت در حال بررسی است. اکنون میخواهیم این آرایه را تبدیل به یک شیء قابل درک برای برنامه کنیم:
validate = () => { const { account } = this.state; const validationResult = Joi.object(this.schema).validate(account, { abortEarly: false }); console.log("validationResult", validationResult); if (!validationResult.error) { return null; } const errors = {}; for (let item of validationResult.error.details) { errors[item.path[0]] = item.message; } return errors; };
اکنون اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، همانند قبل میتوان خطاهای اعتبارسنجی در حین submit را مشاهده کرد:
بازنویسی متد validateProperty توسط Joi
تا اینجا متد validate ساده و ابتدایی خود را با استفاده از امکانات کتابخانهی Joi، بازنویسی کردیم. اکنون نوبت بازنویسی متد اعتبارسنجی در حین تایپ اطلاعات است:
validateProperty = ({ name, value }) => { const userInputObject = { [name]: value }; const propertySchema = Joi.object({ [name]: this.schema[name] }); const { error } = propertySchema.validate(userInputObject, { abortEarly: true }); return error ? error.details[0].message : null; };
تا اینجا بجای this.state.account که به کل فرم اشاره میکند، شیء اختصاصیتر userInputObject را ایجاد کردهایم که معادل اطلاعات فیلد ورودی کاربر است. یک چنین نکتهای را در مورد schema نیز باید رعایت کرد. در اینجا نمیتوان اعتبارسنجی را با this.schema شروع کرد؛ چون این شیء نیز به اطلاعات کل فرم اشاره میکند و نه تک فیلدی که هم اکنون در حال کار با آن هستیم. بنابراین نیاز است یک Joi.object جدید را بر اساس name رسیده، از this.schema کلی، استخراج و تولید کرد؛ مانند propertySchema که مشاهده میکنید.
اکنون میتوان متد validate این شیء اسکیمای جدید را فراخوانی کرد و خاصیت error شیء حاصل از آنرا توسط Object Destructuring، استخراج نمود. در اینجا abortEarly به true تنظیم شدهاست (البته حالت پیشفرض آن است و نیازی به ذکر صریح آن نیست). علت اینجا است که نمایش خطاهای بیش از اندازه به کاربر، برای او گیج کننده خواهند بود؛ به همین جهت هربار، اولین خطای حاصل را به او نمایش میدهیم.
غیرفعالسازی دکمهی submit در صورت شکست اعتبارسنجیهای فرم
در ادامه میخواهیم دکمهی submit فرم لاگین، تا زمانیکه اعتبارسنجی آن با موفقیت به پایان برسد، غیرفعال باشد:
<button disabled={this.validate()} className="btn btn-primary"> Login </button>
فراخوانی setStateهای تعریف شدهی در متدهای رویدادگردان این فرم هستند که سبب رندر مجدد کامپوننت شده و در این بین در متد رندر، کار بررسی مجدد مقدار نهایی متد validate صورت میگیرد تا بر اساس آن، فعال یا غیرفعال بودن دکمهی Login، مشخص شود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-19.zip
public class Rectangle { public double Width; public double Height; public double Area() { return Width*Height; } public double Perimeter() { return 2*(Width + Height); } }
تذکر: ممکن است این سوال پیش بیاید که خوب ما کلاس را نوشته ایم و خودمان میدانیم چه مقادیری برای فیلدهای آن مناسب است. اما مسئله اینجاست که اولاً ممکن است کلاس تهیه شده توسط برنامه نویس دیگری مورد استفاده قرار گیرد. یا حتی پس از مدتی فراموش کنیم چه مقادیری برای کلاسی که مدتی قبل تهیه کردیم مناسب است. و از همه مهمتر این است که کلاسها و اشیاء به عنوان ابزاری برای حل مسائل هستند و ممکن است مقادیری که به فیلدها اختصاص مییابد در زمان نوشتن برنامه مشخص نباشد و در زمان اجرای برنامه توسط کاربر یا کدهای بخشهای دیگر برنامه تعیین گردد.
به طور کلی هر چه کنترل و نظارت بیشتری بر روی مقادیر انتسابی به اشیاء داشته باشیم برنامه بهتر کار میکند و کمتر دچار خطاهای مهلک و بدتر از آن خطاهای منطقی میگردد. بنابراین باید ساز و کار این نظارت را در کلاس تعریف نماییم.
برای کلاسها یک نوع عضو دیگر هم میتوان تعریف کرد که دارای این ساز و کار نظارتی است. این عضو Property نام دارد و یک مکانیسم انعطاف پذیر برای خواندن، نوشتن یا حتی محاسبه مقدار یک فیلد خصوصی فراهم مینماید.
تا اینجا باید به این نتیجه رسیده باشید که تعریف یک متغیر با سطح دسترسی عمومی در کلاس روش پسندیده و قابل توصیه ای نیست. بنابراین متغیرها را در سطح کلاس به صورت خصوصی تعریف میکنیم و از طریق تعریف Property امکان استفاده از آنها در بیرون کلاس را ایجاد میکنیم.
حال به چگونگی تعریف Propertyها دقت کنید.
public class Rectangle { private double _width = 0; private double _height = 0; public double Width { get { return _width; } set { if (value > 0) { _width = value; } } } public double Height { get { return _height; } set { if (value > 0) { _height = value; } } } public double Area() { return _width * _height; } public double Perimeter() { return 2*(_width + _height); } }
دو قسمت اضافه شده دیگر تعریف دو Property مورد نظر است. یکی عرض و دیگری ارتفاع. خط اول تعریف پروپرتی تفاوتی با تعریف فیلد عمومی ندارد. اما همان طور که میبینید هر فیلد دارای یک بدنه است که با {} مشخص میشود. در این بدنه ساز و کار نظارتی تعریف میشود.
نحوه دسترسی به پروپرتیها مشابه فیلدهای عمومی است. اما پروپرتیها در حقیقت متدهای ویژه ای به نام اکسسور (Accessor) هستند که از طرفی سادگی استفاده از متغیرها را به ارمغان میآورند و از طرف دیگر دربردارنده امنیت و انعطاف پذیری متدها هستند. یعنی در عین حال که روشی عمومی برای داد و ستد مقادیر ارایه میدهند، کد پیاده سازی یا وارسی اطلاعات را مخفی نموده و استفاده کننده کلاس را با آن درگیر نمیکنند. قطعه کد زیر چگونگی استفاده از پروپرتی را نشان میدهد.
Rectangle rectangle = new Rectangle(); rectangle.Width = 10; Console.WriteLine(rectangle.Width);
تذکر: به دو اکسسور get و set مانند دو متد معمولی نگاه کنید از این نظر که میتوانید در بدنه آنها اعمال دلخواه دیگری بجز ذخیره و بازیابی اطلاعات پروپرتی را نیز انجام دهید.
چند نکته:
- اکسسور get هنگام بازگشت یا خواندن مقدار پروپرتی اجرا میشود و اکسسور set زمان انتساب یک مقدار جدید به پروپرتی فراخوانی میشود. جالب آنکه در صورت لزوم این دو اکسسور میتوانند دارای سطوح دسترسی متفاوتی باشند.
- داخل اکسسور set کلمه کلیدی value مقدار منتسب شده را در اختیار قرار میدهد تا در صورت لزوم بتوان بر روی آن پردازش لازم را انجام داد.
- یک پروپرتی میتواند فاقد اکسسور set باشد که در این صورت یک پروپرتی فقط خواندنی ایجاد میگردد. همچنین میتواند فقط شامل اکسسور set باشد که در این صورت فقط امکان انتساب مقادر به آن وجود دارد و امکان دریافت یا خواندن مقدار آن میسر نیست. چنین پروپرتی ای فقط نوشتنی خواهد بود.
- در بدنه اکسسور set الزامی به انتساب مقدار منتسب توسط کد مشتری نیست. در صورت صلاحدید میتوانید به جای آن هر مقدار دیگری را در نظر بگیرید یا عملیات مورد نظر خود را انجام دهید.
- در بدنه اکسسور get هم هر مقداری را میتوانید بازگشت دهید. یعنی الزامی وجود ندارد حتماً مقدار فیلد خصوصی متناظر با پروپرتی را بازگشت دهید. حتی الزامی به تعریف فیلد خصوصی برای هر پروپرتی ندارید. به طور مثال ممکن است مقدار بازگشتی اکسسور get حاصل محاسبه و ... باشد.
اکنون مثال دیگری را در نظر بگیرید. فرض کنید در یک پروژه فروشگاهی در حال تهیه کلاسی برای مدیریت محصولات هستید. قصد داریم یک پروپرتی ایجاد کنیم تا نام محصول را نگهداری کند و در حال حاضر هیچ محدودیتی برای نام یک محصول در نظر نداریم. کد زیر را ببینید.
public class Product { private string _name; public string Name { get { return _name; } set { _name = value; } } }
فرض کنید پس از مدتی متوجه شدید اگر نام بسیار طولانی ای برای محصول درج شود ظاهر برنامه شما دچار مشکل میشود. پس باید بر روی این مورد نظارت داشته باشید. دیدیم که برای رسیدن به این هدف باید فیلد عمومی را فراموش و به جای آن پروپرتی تعریف کنیم. اما مسئله اینست که تبدیل یک فیلد عمومی به پروپرتی میتواند سبب بروز ناسازگاریهایی در بخشهای دیگر برنامه که از این کلاس و آن فیلد استفاده میکنند شود. پس بهتر آن است که از ابتدا پروپرتی تعریف کنیم هر چند نیازی به عملیات نظارتی خاصی نداریم. در این حالت اگر نیاز به پردازش بیشتر پیدا شد به راحتی میتوانیم کد مورد نظر را در اکسسورهای موجود اضافه کنیم بدون آنکه نیازی به تغییر بخشهای دیگر باشد.
و یک خبر خوب! از سی شارپ ۳ به بعد ویژگی جدیدی در اختیار ما قرار گرفته است که میتوان پروپرتیهایی مانند مثال بالا را که نیازی به عملیات نظارتی ندارند، سادهتر و خواناتر تعریف نمود. این ویژگی جدید پروپرتی اتوماتیک یا Auto-Implemented Property نام دارد. مانند نمونه زیر.
public class Product { public string Name { get; set; } }
البته استفاده از پروپرتی برتری دیگری هم دارد. و آن کنترل سطح دسترسی اکسسورها است. مثال زیر را ببینید.
public class Student { public DateTime Birthdate { get; set; } public double Age { get; private set; } }
تذکر: در هنگام تعریف یک فیلد میتوان از کلمه کلیدی readonly استفاده کرد تا یک فیلد فقط خواندنی ایجاد گردد. اما در اینصورت فیلد تعریف شده حتی داخل کلاس هم فقط خواندنی است و فقط در هنگام تعریف یا در متد سازنده کلاس امکان مقدار دهی به آن وجود دارد. در بخشهای بعدی مفهوم سازنده کلاس مورد بررسی خواهد گرفت.
new Field { Type = column.DataType }
+ نیازی هم به این همه پیچیدگی نداشت. تمام کارهایی را که انجام دادید با تهیه خروجی ساده <List<Field از یک متد دلخواه، یکی هست و نیازی به anonymous type کار کردن نبود.
- به همان کلاس فیلد، خواص دیگر مورد نیاز را اضافه کنید (عنوان و سایر مشخصات یک فیلد) و در نهایت لیست ساده <List<Field را بازگشت دهید. هر خاصیت کلاس Field، یک ستون گرید را تشکیل میدهد.
- همچنین دقت داشته باشید اگر از روش مطلب جاری استفاده میکنید، اطلاعات ستونهای نهایی باید در فیلد Data نهایی قرار گیرند (قسمت «پیشنیاز تامین داده مخصوص Kendo UI Grid» در بحث).
مراحل کلی سفارشی سازی قالبهای Scaffolding پیش فرض ASP.NET MVC
قالبهای Scaffolding پیش فرض ASP.NET در مسیر Microsoft Visual Studio X\Common7\IDE\ItemTemplates\CSharp\Web\MVC X\CodeTemplates قرار دارند. برای نمونه اگر بخواهیم پیش فرضهای تولید فرمهای MVC4 را تغییر دهیم، باید به پوشه MVC 4\CodeTemplates\AddView\CSHTML مراجعه و فایل Create.tt را ویرایش کنیم.
اینکار هرچند عملی است اما آنچنان جالب نیست؛ از این جهت که تاثیری کلی و سراسری خواهد داشت.
برای اعمال محلی این تغییرات فقط به یک پروژه خاص، تنها کافی است همین مسیر CodeTemplates\AddView\CSHTML به همراه تمام فایلهای tt آن، در پوشه جاری پروژه مدنظر ما کپی شود. به این ترتیب ابتدا به این پوشه محلی مراجعه خواهد شد.
روش دوم کپی کردن این فایلها، استفاده از بسته نیوگت ذیل است:
PM> Install-Package Mvc4CodeTemplatesCSharp
سفارشی سازی فایل Create.tt پیش فرض ASP.NET MVC جهت سازگار سازی آن با Twitter bootstrap
در اینجا قصد داریم همان 8 مرحله مطلب «اعمال کلاسهای ویژه اعتبارسنجی Twitter bootstrap به فرمهای ASP.NET MVC» را به فایل Create.tt که اکنون در پوشه CodeTemplates\AddView\CSHTML\Create.tt ریشه پروژه جاری قرار دارد، اعمال کنیم.
الف) ابتدا نام این فایل را به CreateBootstrapForm.tt تغییر میدهیم. از این لحاظ که این نام جدید در drop down مرتبط با scaffold template صفحه Add view ظاهر خواهد شد. به علاوه نیازی نیست تا این فایل tt در همان لحظه اجرا شود، بنابراین به خواص آن در VS.NET مراجعه کرده و مقدار گزینه custom tool آنرا خالی میکنیم (مانند سایر فایلهای tt اضافه شده).
ب) قسمت ابتدایی فایل CreateBootstrapForm.tt را که همان کپی مطابق اصل فایل Create.tt است، به نحو ذیل تغییر میدهیم:
<# if (!mvcHost.IsContentPage) { #> <script src="~/Scripts/jquery-1.9.1.min.js"></script> <script src="~/Scripts/jquery.validate.min.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script> <# } } #> @using (Html.BeginForm()) { @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" }) <fieldset class="form-horizontal"> <legend><#= mvcHost.ViewDataType.Name #></legend> <# foreach (ModelProperty property in GetModelProperties(mvcHost.ViewDataType)) { if (!property.IsPrimaryKey && !property.IsReadOnly && property.Scaffold) { #> <div class="control-group"> <# if (property.IsForeignKey) { #> @Html.LabelFor(model => model.<#= property.Name #>, "<#= property.AssociationName #>",new {@class="control-label"}) <# } else { #> @Html.LabelFor(model => model.<#= property.Name #>,new {@class="control-label"}) <# } #> <div class="controls"> <# if (property.IsForeignKey) { #> @Html.DropDownList("<#= property.Name #>", String.Empty) <# } else { #> @Html.EditorFor(model => model.<#= property.Name #>) <# } #> @Html.ValidationMessageFor(model => model.<#= property.Name #>,null,new{@class="help-inline"}) </div> </div> <# } } #> <div class="form-actions"> <button type="submit" class="btn btn-primary">ارسال</button> <button class="btn">لغو</button> </div> </fieldset> } <div> @Html.ActionLink("Back to List", "Index") </div> <# if(mvcHost.IsContentPage && mvcHost.ReferenceScriptLibraries) { #> @section JavaScript { }
دریافت فایل CreateBootstrapForm.tt اصلاح شده:
همانطور که عنوان شد، برای استفاده از آن فقط کافی است آنرا در مسیر CodeTemplates\AddView\CSHTML\CreateBootstrapForm.tt ریشه پروژه جاری خود کپی کنید.
پیشنیازها
«بررسی روش آپلود فایلها در ASP.NET Core»
«ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax »
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید مانند UploadFilesSample را ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف و پوشهی خالی wwwroot را نیز به آن اضافه میکنیم.
همچنین بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios react-toastify
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای نمایش پیامهای برنامه از کامپوننت react-toastify استفاده میکنیم که پس از نصب آن، با مراجعه به فایل app.js نیاز است importهای لازم آنرا اضافه کنیم:
import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css";
render() { return ( <React.Fragment> <ToastContainer />
ایجاد کامپوننت React فرم ارسال فایلها به سمت سرور
پس از این مقدمات، فایل جدید clientapp\src\components\UploadFileSimple.jsx را ایجاد کرده و به صورت زیر تکمیل میکنیم:
import React, { useState } from "react"; import axios from "axios"; import { toast } from "react-toastify"; export default function UploadFileSimple() { const [description, setDescription] = useState(""); const [selectedFile1, setSelectedFile1] = useState(); const [selectedFile2, setSelectedFile2] = useState(); return ( <form> <fieldset className="form-group"> <legend>Support Form</legend> <div className="form-group row"> <label className="form-control-label" htmlFor="description"> Description </label> <input type="text" className="form-control" name="description" onChange={event => setDescription(event.target.value)} value={description} /> </div> <div className="form-group row"> <label className="form-control-label" htmlFor="file1"> File 1 </label> <input type="file" className="form-control" name="file1" onChange={event => setSelectedFile1(event.target.files[0])} /> </div> <div className="form-group row"> <label className="form-control-label" htmlFor="file2"> File 2 </label> <input type="file" className="form-control" name="file2" onChange={event => setSelectedFile2(event.target.files[0])} /> </div> <div className="form-group row"> <button className="btn btn-primary" type="submit" > Submit </button> </div> </fieldset> </form> ); }
- توسط آن یک textbox به همراه دو فیلد ارسال فایل، به فرم اضافه شدهاند.
- مرحلهی بعد، دسترسی به فایلهای انتخابی کاربر و همچنین مقدار توضیحات وارد شدهاست. به همین جهت با استفاده از useState Hook، روش دریافت و تنظیم این مقادیر را مشخص کردهایم:
const [description, setDescription] = useState(""); const [selectedFile1, setSelectedFile1] = useState(); const [selectedFile2, setSelectedFile2] = useState();
- پس از طراحی state این فرم، مرحلهی بعدی، استفاده از متدهای set تمام useStateهای فوق است. برای مثال در مورد یک textbox معمولی، میتوان آنرا به صورت inline تعریف کرد و با هر بار تغییری در محتوای آن، این رخداد را به متد setDescription ارسال نمود تا مقدار وارد شده را به متغیر حالت description انتساب دهد:
<input type="text" className="form-control" name="description" onChange={event => setDescription(event.target.value)} value={description} />
<input type="file" className="form-control" name="file1" onChange={event => setSelectedFile1(event.target.files[0])} />
تشکیل مدل ارسال دادهها به سمت سرور
در فرمهای معمولی، عموما دادهها به صورت یک شیء JSON به سمت سرور ارسال میشوند؛ اما در اینجا وضع متفاوت است و به همراه توضیحات وارد شده، دو فایل باینری نیز وجود دارند.
در حالت ارسال متداول فرمهایی که به همراه المانهای دریافت فایل هستند، ابتدا یک ویژگی enctype با مقدار multipart/form-data به المان فرم اضافه میشود و سپس این فرم به سادگی قابلیت post-back به سمت سرور را پیدا میکند:
<form enctype="multipart/form-data" action="/upload" method="post"> <input id="file-input" type="file" /> </form>
let file = document.getElementById("file-input").files[0]; let formData = new FormData(); formData.append("file", file); fetch('/upload/image', {method: "POST", body: formData});
در یک برنامهی React نیز باید دقیقا چنین مراحلی طی شوند. تا اینجا کار دسترسی به مقدار files[0] و تشکیل متغیرهای حالت فرم را انجام دادهایم. در مرحلهی بعد، شیء FormData را تشکیل خواهیم داد:
// ... export default function UploadFileSimple() { // ... const handleSubmit = async event => { event.preventDefault(); const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2); toast.success("Form has been submitted successfully!"); setDescription(""); }; return ( <form onSubmit={handleSubmit}> </form> ); }
ارسال مدل دادههای فرم React به سمت سرور
پس از تشکیل شیء FormData در متد مدیریت کنندهی handleSubmit، اکنون با استفاده از کتابخانهی axios، کار ارسال این اطلاعات را به سمت سرور انجام خواهیم داد:
// ... export default function UploadFileSimple() { const apiUrl = "https://localhost:5001/api/SimpleUpload/SaveTicket"; // ... const [isUploading, setIsUploading] = useState(false); const handleSubmit = async event => { event.preventDefault(); const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2); try { setIsUploading(true); const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }} }); toast.success("Form has been submitted successfully!"); console.log("uploadResult", data); setIsUploading(false); setDescription(""); } catch (error) { setIsUploading(false); toast.error(error); } }; return ( // ... ); }
در قطعه کد فوق، متغیر جدید حالت isLoading را نیز مشاهده میکنید. از آن میتوان برای فعال و غیرفعال کردن دکمهی submit فرم در زمان ارسال اطلاعات به سمت سرور، استفاده کرد:
<button disabled={ isUploading } className="btn btn-primary" type="submit" > Submit </button>
اعتبارسنجی سمت کلاینت فایلهای ارسالی به سمت سرور
در اینجا شاید نیاز باشد نوع و یا اندازهی فایلهای انتخابی توسط کاربر را تعیین اعتبار کرد. به همین جهت متدی را برای اینکار به صورت زیر تهیه میکنیم:
const isFileValid = selectedFile => { if (!selectedFile) { // toast.error("Please select a file."); return false; } const allowedMimeTypes = [ "image/png", "image/jpeg", "image/gif", "image/svg+xml" ]; if (!allowedMimeTypes.includes(selectedFile.type)) { toast.error(`Invalid file type: ${selectedFile.type}`); return false; } const maxFileSize = 1024 * 500; const fileSize = selectedFile.size; if (fileSize > maxFileSize) { toast.error( `File size ${(fileSize / 1024).toFixed( 2 )} KB must be less than ${maxFileSize / 1024} KB` ); return false; } return true; };
اکنون برای استفادهی از این متد دو راه وجود دارد:
الف) استفاده از آن در متد مدیریت کنندهی submit اطلاعات:
const handleSubmit = async event => { event.preventDefault(); if (!isFileValid(selectedFile1) || !isFileValid(selectedFile2)) { return; }
ب) استفادهی از آن جهت غیرفعال کردن دکمهی submit:
<button disabled={ isUploading || !isFileValid(selectedFile1) || !isFileValid(selectedFile2) } className="btn btn-primary" type="submit" > Submit </button>
نمایش درصد پیشرفت آپلود فایلها
کتابخانهی axios، امکان دسترسی به میزان اطلاعات آپلود شدهی به سمت سرور را به صورت یک رخداد فراهم کردهاست که در ادامه از آن برای نمایش درصد پیشرفت آپلود فایلها استفاده میکنیم:
const startTime = Date.now(); const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }, onUploadProgress: progressEvent => { const { loaded, total } = progressEvent; const timeElapsed = Date.now() - startTime; const uploadSpeed = loaded / (timeElapsed / 1000); setUploadProgress({ queueProgress: Math.round((loaded / total) * 100), uploadTimeRemaining: Math.ceil((total - loaded) / uploadSpeed), uploadTimeElapsed: Math.ceil(timeElapsed / 1000), uploadSpeed: (uploadSpeed / 1024).toFixed(2) }); } });
const [uploadProgress, setUploadProgress] = useState({ queueProgress: 0, uploadTimeRemaining: 0, uploadTimeElapsed: 0, uploadSpeed: 0 });
const showUploadProgress = () => { const { queueProgress, uploadTimeRemaining, uploadTimeElapsed, uploadSpeed } = uploadProgress; if (queueProgress <= 0) { return <></>; } return ( <table className="table"> <thead> <tr> <th width="15%">Event</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td> <strong>Elapsed time</strong> </td> <td>{uploadTimeElapsed} second(s)</td> </tr> <tr> <td> <strong>Remaining time</strong> </td> <td>{uploadTimeRemaining} second(s)</td> </tr> <tr> <td> <strong>Upload speed</strong> </td> <td>{uploadSpeed} KB/s</td> </tr> <tr> <td> <strong>Queue progress</strong> </td> <td> <div className="progress-bar progress-bar-info progress-bar-striped" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow={queueProgress} style={{ width: queueProgress + "%" }} > {queueProgress}% </div> </td> </tr> </tbody> </table> ); };
{showUploadProgress()}
در اینجا از کامپوننت progress-bar خود بوت استرپ برای نمایش درصد آپلود فایلها استفاده شدهاست. اگر style آنرا هر بار با مقدار جدید queueProgress به روز رسانی کنیم، سبب نمایش پویای این progress-bar خواهد شد.
یک نکته: اگر میخواهید درصد پیشرفت آپلود را در حالت آزمایش local بهتر مشاهده کنید، دربرگهی network، سرعت را بر روی 3G تنظیم کنید (مانند تصویر ابتدای بحث)؛ در غیراینصورت همان ابتدای کار به علت بالا بودن سرعت ارسال فایلها، 100 درصد را مشاهده خواهید کرد.
دریافت فرم React درخواست پشتیبانی، در سمت سرور و ذخیرهی فایلهای آن
بر اساس نحوهی تشکیل FormData سمت کلاینت:
const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2);
using Microsoft.AspNetCore.Http; namespace UploadFilesSample.Models { public class Ticket { public int Id { set; get; } public string Description { set; get; } public IFormFile File1 { set; get; } public IFormFile File2 { set; get; } } }
پس از آن کنترلر ذخیره سازی اطلاعات Ticket را مشاهده میکنید:
using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using UploadFilesSample.Models; namespace UploadFilesSample.Controllers { [Route("api/[controller]")] [ApiController] public class SimpleUploadController : Controller { private readonly IWebHostEnvironment _environment; public SimpleUploadController(IWebHostEnvironment environment) { _environment = environment; } [HttpPost("[action]")] public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket) { var file1Path = await saveFileAsync(ticket.File1); var file2Path = await saveFileAsync(ticket.File2); //TODO: save the ticket ... get id return Created("", new { id = 1001 }); } private async Task<string> saveFileAsync(IFormFile file) { const string uploadsFolder = "uploads"; var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } //TODO: Do security checks ...! if (file == null || file.Length == 0) { return string.Empty; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(fileStream); } return $"/{uploadsFolder}/{file.Name}"; } } }
- تزریق IWebHostEnvironment در سازندهی کلاس کنترلر، سبب میشود تا از طریق خاصیت WebRootPath آن، به wwwroot دسترسی پیدا کنیم و فایلهای نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه میکنید، هنوز هم model binding کار کرده و میتوان شیء Ticket را به نحو متداولی دریافت کرد:
public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }} });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: UploadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشت شده و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.