ویژگی ها
- امکان ایجاد progress bar و همچنین قابلیت لغو آپلود.
- Drag & Drop
- امکان paste کردن از Clipboard و قابلیت drag & drop از browser
- امکان resize کردن تصویر قبل از آپلود شدن
- امکان تغییر دادن orientation تصویر پیش از ارسال
- قابلیت resume و pause/play در آپلود
- امکان فیلتر کردن type، size و طول و عرض تصاویر
- قابلیت تبدیل به Flash shim در صورت عدم پشتیبانی html5 توسط مرورگر
نصب و پیاده سازی
bower install ng-file-upload-shim --save bower install ng-file-upload --save
<input type="file" ngf-select ng-model="picFile" name="file" accept="image/*" ngf-max-size="2MB" required>
<div ngf-drop ng-model="picFile" ngf-pattern="image/*"> ... </div>
<i ng-show="myForm.file.$error.required">*required</i><br> <i ng-show="myForm.file.$error.maxSize"> File too large: max is 2M </i>
app.controller('MyCtrl', ['$scope', 'Upload', '$timeout', function ($scope, Upload, $timeout) { // the method uploadPic called from Angular View $scope.uploadPic = function(file) { file.upload = Upload.upload({ url: 'https://angular-file-upload-cors-srv.appspot.com/upload', data: {file: file, otherData: $scope.otherData}, }); file.upload.then(function (response) { // waiting for response $timeout(function () { file.result = response.data; }); }, function (response) { //error if (response.status > 0) $scope.errorMsg = response.status + ': ' + response.data; }, function (evt) { //progress file.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total)); console.log(file.progress); }); } }]);
الف) پروژه با خطای زیر بارگذاری نمیشود:
Version conflict detected for Microsoft.AspNetCore.Http.Abstractions. Install/reference Microsoft.AspNetCore.Http.Abstractions 2.2.0 directly to project xyz to resolve this issue.
- ابتدا تمام TargetFrameworkهای موجود را به 2.2 تغییر دهید.
<TargetFramework>netcoreapp2.2</TargetFramework>
dotnet tool update --global dotnet-outdated dotnet outdated -u
A PackageReference to 'Microsoft.AspNetCore.App' specified a Version of `2.2.0`. Specifying the version of this package is not recommended. For more information, see https://aka.ms/sdkimplicitrefs
<PackageReference Include="Microsoft.AspNetCore.App" />
<div> <div> <select ></select> </div> </div>
$(document).ready(function () { $('.js-example-basic-single').select2({ data: [ { id: "al", text: "Albania" }, { id: "dz", text: "Algeria" }, { id: "as", text: "American Samoa" }, { id: "ad", text: "Andorra" }, { id: "ao", text: "Angola" }, { id: "ai", text: "Anguilla" }, { id: "ag", text: "Antigua" }, { id: "ar", text: "Argentina" }, { id: "am", text: "Armenia" }, { id: "aw", text: "Aruba" }, { id: "au", text: "Australia" }, { id: "at", text: "Austria" }, { id: "az", text: "Azerbaijan" }, { id: "bs", text: "Bahamas" }, { id: "bh", text: "Bahrain" }, { id: "bd", text: "Bangladesh" }, { id: "bb", text: "Barbados" }, { id: "by", text: "Belarus" }, { id: "be", text: "Belgium" }, { id: "bz", text: "Belize" }, { id: "bj", text: "Benin" }, { id: "bm", text: "Bermuda" }, { id: "bt", text: "Bhutan" }, { id: "bo", text: "Bolivia" }, { id: "ba", text: "Bosnia" }, { id: "bw", text: "Botswana" }, { id: "bv", text: "Bouvet Island" }, { id: "br", text: "Brazil" }, { id: "vg", text: "British Virgin Islands" }, { id: "bn", text: "Brunei" }, { id: "bg", text: "Bulgaria" }, { id: "bf", text: "Burkina Faso" }, { id: "mm", text: "Burma" }, { id: "bi", text: "Burundi" }, { id: "tc", text: "Caicos Islands" }, { id: "kh", text: "Cambodia" }, { id: "cm", text: "Cameroon" }, { id: "ca", text: "Canada" }, { id: "cv", text: "Cape Verde" }, { id: "ky", text: "Cayman Islands" }, { id: "cf", text: "Central African Republic" }, { id: "td", text: "Chad" }, { id: "cl", text: "Chile" }, { id: "cn", text: "China" }, { id: "cx", text: "Christmas Island" }, { id: "cc", text: "Cocos Islands" }, { id: "co", text: "Colombia" }, { id: "km", text: "Comoros" }, { id: "cg", text: "Congo Brazzaville" }, { id: "cd", text: "Congo" }, { id: "ck", text: "Cook Islands" }, { id: "cr", text: "Costa Rica" }, { id: "ci", text: "Cote Divoire" }, { id: "hr", text: "Croatia" }, { id: "cu", text: "Cuba" }, { id: "cy", text: "Cyprus" }, { id: "cz", text: "Czech Republic" }, { id: "dk", text: "Denmark" }, { id: "dj", text: "Djibouti" }, { id: "dm", text: "Dominica" }, { id: "do", text: "Dominican Republic" }, { id: "ec", text: "Ecuador" }, { id: "eg", text: "Egypt" }, { id: "sv", text: "El Salvador" }, { id: "gb", text: "England" }, { id: "gq", text: "Equatorial Guinea" }, { id: "er", text: "Eritrea" }, { id: "ee", text: "Estonia" }, { id: "et", text: "Ethiopia" }, { id: "eu", text: "European Union" }, { id: "fk", text: "Falkland Islands" }, { id: "fo", text: "Faroe Islands" }, { id: "fj", text: "Fiji" }, { id: "fi", text: "Finland" }, { id: "fr", text: "France" }, { id: "gf", text: "French Guiana" }, { id: "pf", text: "French Polynesia" }, { id: "tf", text: "French Territories" }, { id: "ga", text: "Gabon" }, { id: "gm", text: "Gambia" }, { id: "ge", text: "Georgia" }, { id: "de", text: "Germany" }, { id: "gh", text: "Ghana" }, { id: "gi", text: "Gibraltar" }, { id: "gr", text: "Greece" }, { id: "gl", text: "Greenland" }, { id: "gd", text: "Grenada" }, { id: "gp", text: "Guadeloupe" }, { id: "gu", text: "Guam" }, { id: "gt", text: "Guatemala" }, { id: "gw", text: "Guinea-Bissau" }, { id: "gn", text: "Guinea" }, { id: "gy", text: "Guyana" }, { id: "ht", text: "Haiti" }, { id: "hm", text: "Heard Island" }, { id: "hn", text: "Honduras" }, { id: "hk", text: "Hong Kong" }, { id: "hu", text: "Hungary" }, { id: "is", text: "Iceland" }, { id: "in", text: "India" }, { id: "io", text: "Indian Ocean Territory" }, { id: "id", text: "Indonesia" }, { id: "ir", text: "Iran" }, { id: "iq", text: "Iraq" }, { id: "ie", text: "Ireland" }, { id: "il", text: "Israel" }, { id: "it", text: "Italy" }, { id: "jm", text: "Jamaica" }, { id: "jp", text: "Japan" }, { id: "jo", text: "Jordan" }, { id: "kz", text: "Kazakhstan" }, { id: "ke", text: "Kenya" }, { id: "ki", text: "Kiribati" }, { id: "kw", text: "Kuwait" }, { id: "kg", text: "Kyrgyzstan" }, { id: "la", text: "Laos" }, { id: "lv", text: "Latvia" }, { id: "lb", text: "Lebanon" }, { id: "ls", text: "Lesotho" }, { id: "lr", text: "Liberia" }, { id: "ly", text: "Libya" }, { id: "li", text: "Liechtenstein" }, { id: "lt", text: "Lithuania" }, { id: "lu", text: "Luxembourg" }, { id: "mo", text: "Macau" }, { id: "mk", text: "Macedonia" }, { id: "mg", text: "Madagascar" }, { id: "mw", text: "Malawi" }, { id: "my", text: "Malaysia" }, { id: "mv", text: "Maldives" }, { id: "ml", text: "Mali" }, { id: "mt", text: "Malta" }, { id: "mh", text: "Marshall Islands" }, { id: "mq", text: "Martinique" }, { id: "mr", text: "Mauritania" }, { id: "mu", text: "Mauritius" }, { id: "yt", text: "Mayotte" }, { id: "mx", text: "Mexico" }, { id: "fm", text: "Micronesia" }, { id: "md", text: "Moldova" }, { id: "mc", text: "Monaco" }, { id: "mn", text: "Mongolia" }, { id: "me", text: "Montenegro" }, { id: "ms", text: "Montserrat" }, { id: "ma", text: "Morocco" }, { id: "mz", text: "Mozambique" }, { id: "na", text: "Namibia" }, { id: "nr", text: "Nauru" }, { id: "np", text: "Nepal" }, { id: "an", text: "Netherlands Antilles" }, { id: "nl", text: "Netherlands" }, { id: "nc", text: "New Caledonia" }, { id: "pg", text: "New Guinea" }, { id: "nz", text: "New Zealand" }, { id: "ni", text: "Nicaragua" }, { id: "ne", text: "Niger" }, { id: "ng", text: "Nigeria" }, { id: "nu", text: "Niue" }, { id: "nf", text: "Norfolk Island" }, { id: "kp", text: "North Korea" }, { id: "mp", text: "Northern Mariana Islands" }, { id: "no", text: "Norway" }, { id: "om", text: "Oman" }, { id: "pk", text: "Pakistan" }, { id: "pw", text: "Palau" }, { id: "ps", text: "Palestine" }, { id: "pa", text: "Panama" }, { id: "py", text: "Paraguay" }, { id: "pe", text: "Peru" }, { id: "ph", text: "Philippines" }, { id: "pn", text: "Pitcairn Islands" }, { id: "pl", text: "Poland" }, { id: "pt", text: "Portugal" }, { id: "pr", text: "Puerto Rico" }, { id: "qa", text: "Qatar" }, { id: "re", text: "Reunion" }, { id: "ro", text: "Romania" }, { id: "ru", text: "Russia" }, { id: "rw", text: "Rwanda" }, { id: "sh", text: "Saint Helena" }, { id: "kn", text: "Saint Kitts and Nevis" }, { id: "lc", text: "Saint Lucia" }, { id: "pm", text: "Saint Pierre" }, { id: "vc", text: "Saint Vincent" }, { id: "ws", text: "Samoa" }, { id: "sm", text: "San Marino" }, { id: "gs", text: "Sandwich Islands" }, { id: "st", text: "Sao Tome" }, { id: "sa", text: "Saudi Arabia" }, { id: "sn", text: "Senegal" }, { id: "cs", text: "Serbia" }, { id: "rs", text: "Serbia" }, { id: "sc", text: "Seychelles" }, { id: "sl", text: "Sierra Leone" }, { id: "sg", text: "Singapore" }, { id: "sk", text: "Slovakia" }, { id: "si", text: "Slovenia" }, { id: "sb", text: "Solomon Islands" }, { id: "so", text: "Somalia" }, { id: "za", text: "South Africa" }, { id: "kr", text: "South Korea" }, { id: "es", text: "Spain" }, { id: "lk", text: "Sri Lanka" }, { id: "sd", text: "Sudan" }, { id: "sr", text: "Suriname" }, { id: "sj", text: "Svalbard" }, { id: "sz", text: "Swaziland" }, { id: "se", text: "Sweden" }, { id: "ch", text: "Switzerland" }, { id: "sy", text: "Syria" }, { id: "tw", text: "Taiwan" }, { id: "tj", text: "Tajikistan" }, { id: "tz", text: "Tanzania" }, { id: "th", text: "Thailand" }, { id: "tl", text: "Timorleste" }, { id: "tg", text: "Togo" }, { id: "tk", text: "Tokelau" }, { id: "to", text: "Tonga" }, { id: "tt", text: "Trinidad" }, { id: "tn", text: "Tunisia" }, { id: "tr", text: "Turkey" }, { id: "tm", text: "Turkmenistan" }, { id: "tv", text: "Tuvalu" }, { id: "ug", text: "Uganda" }, { id: "ua", text: "Ukraine" }, { id: "ae", text: "United Arab Emirates" }, { id: "us", text: "United States" }, { id: "uy", text: "Uruguay" }, { id: "um", text: "Us Minor Islands" }, { id: "vi", text: "Us Virgin Islands" }, { id: "uz", text: "Uzbekistan" }, { id: "vu", text: "Vanuatu" }, { id: "va", text: "Vatican City" }, { id: "ve", text: "Venezuela" }, { id: "vn", text: "Vietnam" }, { id: "wf", text: "Wallis and Futuna" }, { id: "eh", text: "Western Sahara" }, { id: "ye", text: "Yemen" }, { id: "zm", text: "Zambia" }, { id: "zw", text: "Zimbabwe" } ], placeholder: "کشور مورد نظرتان را انتخاب نمایید", language: "fa", theme: "bootstrap", dir: "rtl", tokenSeparators: [',', ' '], multiple: false, templateResult:format, templateSelection: format, escapeMarkup: function (m) { return m; } }); }); </script>
function format(state) { var $state = $( '<span>' + state.text + ' <i class="' + state.id + ' flag"> ' + '</i></span>' ); return $state; };
<i class="ir flag"></i>
Cargo چیست و چه کاربردی دارد؟
Cargo همراه با زبان برنامه نویسی Rust گنجانده شده، همزمان نصب میشود و برای ایجاد، ساخت و مدیریت پروژههای Rust استفاده میگردد. این یک رابط سطح بالا برای کار با کدهای Rust را ارائه میدهد که شروع به کار با Rust و مدیریت پروژههای خود را برای توسعه دهندگان آسانتر میکند.
Cargo سیستم ساخت و package manager مخصوص زبان برنامه نویسی Rust است. ابزاری است که به توسعه دهندگان Rust کمک میکند تا پروژههای خود را با خودکارسازی کارهایی مانند کامپایل کد، مدیریت وابستگیها، اجرای آزمایشها و ایجاد بستههای قابل توزیع، مدیریت کنند.
Dependency management: برنامه Cargo میتواند بهطور خودکار وابستگیهای پروژههای Rust را دانلود کرده، بسازد و مدیریت کند. این باعث میشود توسعه دهندگان به راحتی کتابخانهها و ماژولهای جدیدی را به پروژههای خود اضافه کنند.
Building and testing: برنامه Cargo میتواند پروژههای Rust را بسازد و testها را به صورت خودکار اجرا کند. همچنین گزینههایی را برای ساختن ساختهای بهینه یا اشکال زدایی فراهم میکند.
Packaging: برنامه Cargo میتواند بستههای قابل توزیعی را مانند tarballs یا بستههای باینری را برای پروژههای Rust ایجاد کند.
Customization: برنامه Cargo به توسعه دهندگان اجازه میدهد تا فرآیند ساخت برنامهی خود را با تعیین گزینههای ساخت مختلف، در فایل پیکربندی Cargo.toml سفارشی کنند. بهطور کلی Cargo توسعه و مدیریت پروژههای Rust را با ارائه یک ابزار کارآمد برای خودکارسازی بسیاری از وظایف توسعه رایج، ساده میکند.
cargo new project_name
Cargo.toml : این فایل manifest پروژه است که در آن نام پروژه، نسخه، وابستگیها و سایر ابردادهها را مشخص میکنید. فایل Cargo.toml حاوی یک metadata درباره پروژه است؛ مانند نام، نسخه، نویسندگان و وابستگیهای آن. در اینجا مثالی از شکل ظاهری یک فایل Cargo.toml آورده شده است:
[package] name = "my-project" version = "0.1.0" authors = ["John Doe <johndoe@example.com>"] [dependencies] serde = "1.0" serde_json = "1.0"
بخش [dependencies] وابستگیهای پروژه را فهرست میکند. در این مثال، پروژه به پکیجهای serde و serde_json بستگی دارد که برای serialization و deserialization دادهها استفاده میشوند.
src directory: این دایرکتوری حاوی کد منبع پروژه شما است. به طور پیش فرض، شامل یک فایل main.rs است که نقطه ورود برنامه شما است.
target directory: این فهرست شامل فایلهای باینری کامپایل شده تولید شده توسط کامپایلر Rust میباشد.
هنگامی که cargo build یا cargo run را اجرا میکنید، Cargo به طور خودکار یک دایرکتوری target/debug را برای ذخیرهی فایلهای باینری کامپایل شده ایجاد میکند. اگر cargo build --release را اجرا کنید، Cargo بجای آن، یک دایرکتوری target/release را ایجاد میکند. بعلاوه، اگر هنگام ایجاد پروژهی خود از نسخهی خاصی از Rust (مانند نسخهی 2018) استفاده کنید، Cargo یک فیلد نسخه را در فایل Cargo.toml شما برای تعیین نسخه، اضافه میکند.
پیش نیازها
- شروع یک پروژهی جدید وب با پشتیبانی از Web API
- نصب دو بستهی نیوگت مرتبط با Structure map 3
PM>install-package structuremap PM>install-package structuremap.web
پیاده سازی IHttpControllerActivator توسط Structure map
using System; using System.Net.Http; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using StructureMap; namespace WebApiDISample.Core { public class StructureMapHttpControllerActivator : IHttpControllerActivator { private readonly IContainer _container; public StructureMapHttpControllerActivator(IContainer container) { _container = container; } public IHttpController Create( HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) { var nestedContainer = _container.GetNestedContainer(); request.RegisterForDispose(nestedContainer); return (IHttpController)nestedContainer.GetInstance(controllerType); } } }
نکتهی مهم آن استفاده از NestedContainer آن است. معرفی آن به متد request.RegisterForDispose سبب میشود تا کلیه کلاسهای IDisposable نیز در پایان کار به صورت خودکار رها سازی شده و نشتی حافظه رخ ندهد.
معرفی StructureMapHttpControllerActivator به برنامه
فایل WebApiConfig.cs را گشوده و تغییرات ذیل را در آن اعمال کنید:
using System.Web.Http; using System.Web.Http.Dispatcher; using StructureMap; using WebApiDISample.Core; using WebApiDISample.Services; namespace WebApiDISample { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // IoC Config ObjectFactory.Configure(c => c.For<IEmailsService>().Use<EmailsService>()); var container = ObjectFactory.Container; GlobalConfiguration.Configuration.Services.Replace( typeof(IHttpControllerActivator), new StructureMapHttpControllerActivator(container)); // Web API routes config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
تهیه سرویسی برای آزمایش برنامه
namespace WebApiDISample.Services { public interface IEmailsService { void SendEmail(); } } using System; namespace WebApiDISample.Services { /// <summary> /// سرویسی که دارای قسمت دیسپوز نیز هست /// </summary> public class EmailsService : IEmailsService, IDisposable { private bool _disposed; ~EmailsService() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void SendEmail() { //todo: send email! } protected virtual void Dispose(bool disposeManagedResources) { if (_disposed) return; if (!disposeManagedResources) return; //todo: clean up resources here ... _disposed = true; } } }
نکتهی مهم آن استفاده از IDisposable در این کلاس خاص است (ضروری نیست؛ صرفا جهت بررسی بیشتر اضافه شدهاست). اگر در کدهای برنامه، یک چنین کلاسی وجود داشت، نیاز است متد Dispose آن نیز توسط IoC Container فراخوانی شود. برای آزمایش آن یک break point را در داخل متد Dispose قرار دهید.
استفاده از سرویس تعریف شده در یک Web API Controller
using System.Web.Http; using WebApiDISample.Services; namespace WebApiDISample.Controllers { public class ValuesController : ApiController { private readonly IEmailsService _emailsService; public ValuesController(IEmailsService emailsService) { _emailsService = emailsService; } // GET api/values/5 public string Get(int id) { _emailsService.SendEmail(); return "_emailsService.SendEmail(); called!"; } } }
تزریق وهلهی مورد نیاز آن، به صورت خودکار توسط StructureMapHttpControllerActivator که در ابتدای بحث معرفی شد، صورت میگیرد.
فراخوانی متد Get آنرا نیز توسط کدهای سمت کاربر ذیل انجام خواهیم داد:
<h2>Index</h2> @section scripts { <script type="text/javascript"> $(function () { $.getJSON('/api/values/1?timestamp=' + new Date().getTime(), function (data) { alert(data); }); }); </script> }
اکنون برنامه را اجرا کنید. هنگام فراخوانی متد Get، وهلهی سرویس مورد نظر، نال نیست. همچنین متد Dispose نیز به صورت خودکار فراخوانی میشود.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
WebApiDISample.zip
بررسی Semantic Search و FTS Table-valued functions
(زبان عربی در FTS پشتیبانی میشود؛ اما نه در Semantic Search)
SELECT * FROM sys.fulltext_semantic_languages ORDER BY name
دوراهی انتخاب NHibernate و Entityframework
در یکی از پروژهها که بانک اطلاعاتی oracle و نرم افزار asp.net بود مجبور شدیم از همان sqlHelperها استفاده کنیم. حتی در یک برنامه تستی برای یک select ساده از یک جدول با یک میلیون رکورد تفاوت فاحشی بین ado.net و EF5 وجود داشت (100 میلی ثانیه در مقابل 2 ثانیه با رعایت Initializeهای EF در حالی که خروجی EF در پروفایلر هم همان کوئری بود).
امکان خروجی اکسل از گزارشات سیستم، یکی از بایدهای بیشتر سیستمهای اطلاعاتی میباشد؛ یکی از چالشهای اصلی در تولید این نوع خروجی، افزایش مصرف حافظه متناسب با افزایش حجم دیتا میباشد. از آنجاییکه بیشتر راهکارهای موجود از جمله ClosedXml یا Epplus کل ساختار را ابتدا تولید کرده و اصطلاحا خروجی مورد نظر را بافر میکنند، برای حجم بالای اطلاعات مناسب نخواهند بود. راهکار برای خروجی CSV به عنوان مثال خیلی سرراست میباشد و میتوان با چند خط کد، به نتیجه دلخواه از طریق مکانیزم Streaming رسید؛ ولی ساختار Excel به سادگی فرمت CSV نیست و برای مثال فرمت Excel Workbook با پسوند xlsx یک بسته Zip شدهای از فایلهای XML میباشد.
معرفی MiniExcel
MiniExcel یک کتابخانه سورس باز با هدف به حداقل رساندن مصرف حافظه در زمان پردازش فایلهای Excel در دات نت میباشد. در مقایسه با Aspose از منظر امکانات شاید حرفی برای گفتن نداشته باشد، ولی از جهت خواندن اطلاعات فایلهای Excel با قابلیت پشتیبانی از LINQ و Deferred Execution در کنار مصرف کم حافظه و جلوگیری از مشکل OOM خیلی خوب عمل میکند. در تصویر زیر مشخص است که برای عمده عملیات پیادهسازی شده، از استریمها بهره برده شده است.
همچنین در زیر مقایسهای روی خروجی ۱ میلیون رکورد با تعداد ۱۰ ستون در هر ردیف انجام شدهاست که قابل توجه میباشد:
Logic : create a total of 10,000,000 "HelloWorld" excel
LibraryMethodMax Memory UsageMean | |||
MiniExcel | 'MiniExcel Create Xlsx' | 15 MB | 11.53181 sec |
Epplus | 'Epplus Create Xlsx' | 1,204 MB | 22.50971 sec |
OpenXmlSdk | 'OpenXmlSdk Create Xlsx' | 2,621 MB | 42.47399 sec |
ClosedXml | 'ClosedXml Create Xlsx' | 7,141 MB | 140.93992 sec |
به شدت API خوش دستی برای استفاده دارد و شاید مطالعه سورس کد آن از جهت طراحی نیز درس آموزی داشته باشد. در ادامه چند مثال از مستندات آن را میتوانید ملاحظه کنید:
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); MiniExcel.SaveAs(path, new[] { new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} });
// DataReader export multiple sheets (recommand by Dapper ExecuteReader) using (var cnn = Connection) { cnn.Open(); var sheets = new Dictionary<string,object>(); sheets.Add("sheet1", cnn.ExecuteReader("select 1 id")); sheets.Add("sheet2", cnn.ExecuteReader("select 2 id")); MiniExcel.SaveAs("Demo.xlsx", sheets); }
طراحی یک ActionResult سفارشی برای استفاده از MiniExcel
برای این منظور نیاز است تا Stream مربوط به Response درخواست جاری را در اختیار این کتابخانه قرار دهیم و از سمت دیگر دیتای مورد نیاز را به نحوی که بافر نشود و از طریق مکانیزم Streaming در EF (استفاده از Deferred Execution و Enumerableها) مهیا کنیم. برای امکان تعویض پذیری (این سناریو در پروژه واقعی و باتوجه به جهت وابستگیها میتواند ضروری باشد) از دو واسط زیر استفاده خواهیم کرد:
public interface IExcelDocumentFactory { ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> headers, Stream stream); } public interface ILargeExcelDocument : IAsyncDisposable, IDisposable { Task Write<T>( PaginatedEnumerable<T> items, int count, int sizeLimit, CancellationToken cancellationToken = default) where T : notnull; }
متد CreateLargeDocument یک وهله از ILargeExcelDocument را در اختیار مصرف کننده قرار میدهد که قابلیت نوشتن روی آن از طریق متد Write را خواهد داشت. روش واکشی دیتا از طریق Delegate تعریف شده با نام PaginatedEnumerable به مصرف کننده محول شدهاست که در ادامه امضای آن را میتوانید مشاهده کنید:
public delegate IEnumerable<T> PaginatedEnumerable<out T>(int page, int pageSize);
در ادامه پیادهسازی واسط ILargeExcelDocument برای MiniExcel به شکل زیر خواهد بود:
internal sealed class MiniExcelDocument(Stream stream, IEnumerable<ExcelColumn> columns) : ILargeExcelDocument { private const int SheetLimit = 1_048_576; private bool _disposedValue; public async Task Write<T>( PaginatedEnumerable<T> items, int count, int sizeLimit, CancellationToken cancellationToken = default) where T : notnull { ThrowIfDisposed(); // TODO: apply sizeLimit var properties = FastReflection.Instance.GetProperties(typeof(T)) .ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); var sheets = new Dictionary<string, object>(); var index = 1; while (count > 0) { cancellationToken.ThrowIfCancellationRequested(); IEnumerable<Dictionary<string, object>> reader = items(index, SheetLimit) .Select(item => { cancellationToken.ThrowIfCancellationRequested(); return columns.ToDictionary(h => h.Title, h => ValueOf(item, h.Name, properties)); }); sheets.Add($"sheet_{index}", reader); count -= SheetLimit; index++; } // This part is forward-only, and we are pretty sure that streaming will happen without buffering. await stream.SaveAsAsync(sheets, cancellationToken: cancellationToken); } private void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects) } // TODO: free unmanaged resources (unmanaged objects) and override finalizer // TODO: set large fields to null _disposedValue = true; } } ~MiniExcelDocument() { Dispose(disposing: false); } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } public async ValueTask DisposeAsync() { Dispose(); await ValueTask.CompletedTask; } private void ThrowIfDisposed() { if (!_disposedValue) return; throw new ObjectDisposedException(nameof(MiniExcelDocument)); } private static object ValueOf<T>(T record, string prop, IDictionary<string, FastPropertyInfo> properties) where T : notnull { var property = properties[prop] ?? throw new InvalidOperationException($"There is no property with given name [{prop}]"); return NormalizeValue(property.GetValue?.Invoke(record)); } private static object NormalizeValue(object? value) { if (value == null) return null!; return value switch { DateTime dateTime => dateTime.ToShortPersianDateTimeString(), TimeSpan time => time.ToString(@"hh\:mm\:ss"), DateOnly dateTime => dateTime.ToShortPersianDateString(false), TimeOnly time => time.ToString(@"hh\:mm\:ss"), bool boolean => boolean ? "بلی" : "خیر", IEnumerable<object> values => string.Join(',', values.Select(NormalizeValue).ToList()), Enum enumField => enumField.GetEnumStringValue(), _ => value }; } }
در بدنه متد Write باتوجه به تعداد کل رکوردها، یک کوئری برای هر شیت از طریق فراخوانی متد منتسب به پارامتر items اجرا خواهد شد؛ توجه کنید که اجرای این کوئری مشخصا به تعویق افتاده و تا زمان اولین MoveNext، اجرایی صورت نخواهد گرفت (مفهوم Deferred Execution). به این ترتیب باقی کارها از جمله فرمت کردن مقادیر در سمت برنامه و از طریق Linq To Object انجام خواهد شد. همچنین پیادهسازی Factory مرتبط با آن به شکل زیر خواهد بود:
internal sealed class ExcelDocumentFactory : IExcelDocumentFactory { public ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> columns, Stream stream) { return new MiniExcelDocument(stream, columns); } }
در ادامه ActionResult سفارشی برای گرفتن خروجی اکسل را به شکل زیر می توان پیادهسازی کرد:
public class ExcelExportResult<T>(PaginatedEnumerable<T> items, int count, ExportMetadata metadata) : ActionResult where T : notnull { private const string ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; private const string Extension = ".xlsx"; private const int SizeLimit = int.MaxValue; private readonly IReadOnlyList<FastPropertyInfo> _properties = FastReflection.Instance.GetProperties(typeof(T)); public override async Task ExecuteResultAsync(ActionContext context) { var sp = context.HttpContext.RequestServices; var factory = sp.GetRequiredService<IExcelDocumentFactory>(); var disposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment); disposition.SetHttpFileName(MakeFilename()); context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = disposition.ToString(); context.HttpContext.Response.Headers.Append(HeaderNames.ContentType, ContentType); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; //TODO: deal with exception, because our global exception handling cannot take into account while the response is started. await using var bodyStream = context.HttpContext.Response.BodyWriter.AsStream(); await context.HttpContext.Response.StartAsync(context.HttpContext.RequestAborted); await using (var document = factory.CreateLargeDocument(MakeColumns(), bodyStream)) { await document.Write(items, count, SizeLimit, context.HttpContext.RequestAborted); } await context.HttpContext.Response.CompleteAsync(); } private string MakeFilename() { return $"{metadata.Title} - {DateTime.UtcNow.ToEpochSeconds()}{Extension}"; } private IEnumerable<ExcelColumn> MakeColumns() { var types = _properties.ToDictionary(p => p.Name, p => p.PropertyType, StringComparer.OrdinalIgnoreCase); return metadata.Fields.Select(f => { var type = types[f.Name]; type = Nullable.GetUnderlyingType(type) ?? type; if (type.IsEnum || type == typeof(DateOnly) || type == typeof(TimeOnly) || type == typeof(bool) || type == typeof(TimeSpan) || type == typeof(DateTime)) { type = typeof(string); } return new ExcelColumn(f.Name, f.Title, type); }); } }
در اینجا از طریق ExportMetadata که از سمت کاربر تعیین میشود، مشخص خواهد شد که کدام فیلدها در فایل نهایی حضور داشته باشند. در بدنه متد ExecuteResultAsync یکسری هدر مرتبط با کار با فایلها تنظیم شدهاست و سپس از طریق BodyWriter و متد AsStream به استریم مورد نظر دست یافته و در اختیار متد Write مربوط به document ایجاد شده، قرار دادهایم. یک نمونه استفاده از آن برای موجودیت فرضی مشتری می تواند به شکل زیر باشد:
[ApiController, Route("api/customers")] public class CustomersController(IDbContext dbContext) : ControllerBase { [HttpGet("export")] public async Task<ActionResult> ExportCustomers([FromQuery] ExportMetadata metadata, CancellationToken cancellationToken) { var count = await dbContext.Set<Customer>().CountAsync(cancellationToken); return this.Export( (page, pageSize) => dbContext.Set<Customer>() .OrderBy(c => c.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .AsNoTracking() .AsEnumerable(), // Enable streaming instead of buffering through deferred execution count, metadata); } }
در اینجا از طریق Extension Method مهیا شده روش کوئری کردن برای هر شیت را مشخص کردهایم؛ نکته مهم در ایجاد استفاده از متد AsEnumerable می باشد که در عمل یک Type Casting انجام می دهد که باقی متدهای استفاده شده روی خروجی، از طریق Linq To Object اعمال شود و همچنین نیاز به استفاده از ToList و یا موارد مشابه را نخواهیم داشت. نمونه درخواست GET برای این API می تواند به شکل زیر باشد:
http://localhost:5118/api/customers/export?Title=Test&Fields[0].Name=FirstName&Fields[0].Title=First name&Fields[1].Name=LastName&Fields[1].Title=Last name&Fields[2].Name=BirthDate&Fields[2].Title=BirthDate
سورس کد مثال قابل اجرا از طریق مخزن زیر قابل دسترس می باشد:
https://github.com/rabbal/large-excel-streaming
در این مثال در زمان آغاز برنامه، ۱۰ میلیون رکورد در جدول Customer ثبت خواهد شد که در ادامه می توان از آن خروجی Excel تهیه کرد.
نکته مهم: توجه داشته باشید که استفاده از این روش قابلیت از سرگیری مجدد برای دانلود را نخواهد داشت و شاید بهتر است این فرآیند را از طریق یک Job انجام داده و با استفاده از قابلیتهای Multipart Upload مربوط به یک BlobStroage مانند Minio، خروجی مورد نظر از قبل ذخیره کرده و لینک دانلودی را در اختیار کاربر قرار دهید.
آموزش (jQuery) جی کوئری 5#
اما هنوز یه ایراد کوچولو داره و اونهم اینه که بعد از رسیدن به اخرین عکس برمیگرده به اول یعنی بصورت بک اسلاید میشه و اگر عکسها از سمت راست به چپ اسلاید میشوند وقتی به اخرین عکس میرسه تمام عکسها در کسری از ثانیه از چپ به راست برمیگردند. نمونه کد کوئری رو میزارم و ممنون میشم منو در این زمینه راهنمایی کنید که چطور کاری کنم با رسیدن به اخرین عکس به همون روش از سمت راست به چپ دوباره برگرده به عکس اول نه تمام عکسها رو از چپ به راست برگردونه ؟
اسکریپت فراخوانده شده :
< script src = "http://code.jquery.com/jquery-latest.js" ></ script >
<script type = "text/javascript" > $(document).ready(function () { slideShow(); }); var n = 0; function slideShow() { id = n % 5 + 1; leftpost = (1 - parseInt(id)) * 500 + "px"; $("div.slider-item").animate({ left: leftpost }, 1500); n = n + 1; s = setTimeout("slideShow()", 3000); } </ script >
<style type = "text/css" > div#slider { width: 500px; height: 300px; margin: auto; overflow: hidden; border: 10px solid gray; } div#slider-mask { width: 500%; height: 100%; } div.slider-item { width: 20%; height: 100%; position: relative; float: left; } </ style >
< div id = "slider" > < div id = "slider-mask" > < div class = "slider-item" >< img src = "img1.jpg" alt = "1" /></ div > < div class = "slider-item" >< img src = "img2.jpg" alt = "2" /></ div > < div class = "slider-item" >< img src = "img3.jpg" alt = "3" /></ div > < div class = "slider-item" >< img src = "img4.jpg" alt = "4" /></ div > < div class = "slider-item" >< img src = "img5.jpg" alt = "5" /></ div > </ div > </ div >