- EF Core 1.0.1 منتشر شد. با این تغییرات
- برای نصب آن و به روز رسانی بستههای نیوگت مرتبط باید NET Core 1.0.1. را هم ابتدا نصب کنید. به علاوه فایل global.json را هم باید به روز رسانی کنید. اطلاعات بیشتر
{ "projects": [ "src", "test" ], "sdk": { "version": "1.0.0-preview2-003131" } }
بحثی در مورد تغییرات اخیر سایت Medium
javascript:location.href='https://webcache.googleusercontent.com/search?q=cache:'+encodeURIComponent(location.href)
EasyCompressor : کتابخانه فشرده سازی با الگوریتم های مختلف
ردیس ۷ در راه است
گویا ۳ برابر سریعتر از الستیکسرچ است!
In Redis 7.0, Redis Labs is adding two enhancements to its JSON support. The first is with search. RediSearch 2.0, which itself only became generally available barely a month ago; it now adds JSON as a supported data type. Before this, search was run as a separate feature that sat apart from the nodes housing the data engine. RediSearch 2.0 adds new scale-out capabilities to conduct massively parallel searches across up to billions of JSON documents across multiple nodes, returning results in fractions of a second. As the search engine was optimized for the Redis database, Redis Labs claims it runs up to 3x faster than Elasticsearch.
مشکل حذف تصاویر آپلود شده
در قسمت قبل، این امکان را مهیا کردیم که کاربران بتوانند پیش از ثبت اطلاعات یک اتاق، تصاویر آنرا به سرور آپلود کنند. یعنی تصاویری که در ابتدای کار آپلود میشوند، هنوز در بانک اطلاعاتی ثبت نشدهاند و هیچ رکوردی از آنها موجود نیست. در این حالت اگر کاربری تصاویری را آپلود کرده و سپس بر روی دکمهی back کلیک کند، با تعدادی تصویر آپلود شدهی غیرمنتسب به اتاقهای موجود، مواجه خواهیم شد. همچنین اگر شخصی به قسمت ویرایش تصاویر مراجعه کند و با کلیک بر روی دکمهی حذف یک تصویر، آنرا حذف کند، این حذف باید در بانک اطلاعاتی هم منعکس شود؛ در غیر اینصورت باز هم کاربر میتواند تصویری را حذف کند، اما در آخر بر روی دکمهی به روز رسانی اطلاعات رکورد کلیک نکند. در این حالت در دفعات بعدی مراجعهی به اطلاعات یک چنین اتاقی، با نقص اطلاعات تصاویری مواجه میشویم که در لیست تصاویر منتسب به یک اتاق وجود دارند، اما اصل فایل تصویری متناظر با آنها از سرور حذف شدهاست.
حذف اطلاعات تصاویر، در حالت ثبت اطلاعات
زمانیکه قرار است اطلاعات اتاقی برای اولین بار ثبت شود، حذف تصاویر آپلود شدهی مرتبط با آن سادهاست؛ چون هنوز اصل رکورد اتاق ثبت نشدهاست و این تصاویر در این لحظه، به رکوردی تعلق ندارند. بنابراین ابتدا متد رویدادگردان DeletePhoto را به دکمهی حذف اطلاعات هر تصویر نمایش داده شده، انتساب میدهیم:
@if (HotelRoomModel.HotelRoomImages.Count > 0) { var serial = 1; foreach (var roomImage in HotelRoomModel.HotelRoomImages) { <div class="col-md-2 mt-3"> <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; "> <span class="room-image-title">@serial</span> </div> <button type="button" @onclick="()=>DeletePhoto(roomImage)" class="btn btn-outline-danger btn-block mt-4">Delete</button> </div> serial++; } }
@code { private const string UploadFolder = "Uploads"; private void DeletePhoto(HotelRoomImageDTO imageDto) { var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase); if (HotelRoomModel.Id == 0 && Title == "Create") { FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder); HotelRoomModel.HotelRoomImages.Remove(imageDto); } } }
- در این شیء، مقدار خاصیت RoomImageUrl، همواره با نام پوشهای که فایلهای تصویری در آن آپلود شدهاند، شروع میشود. به همین جهت نام پوشه را از آن حذف کرده و بر این اساس، متد FileUploadService.DeleteFile را فراخوانی میکنیم تا تصویر جاری را از سرور حذف کند.
- سپس با فراخوانی متد Remove بر روی لیست تصاویر موجود، سبب به روز رسانی UI نیز خواهیم شد و به این ترتیب، تصویری که فایل آن از سرور حذف شده، از UI نیز حذف خواهد شد.
حذف تصاویر، در زمان ویرایش اطلاعات یک اتاق تعریف شده
همانطور که در ابتدای بحث نیز عنوان شد، نمیخواهیم در حالت ویرایش یک رکورد، با کلیک بر روی حذف یک تصویر، بلافاصله آنرا از سرور نیز حذف کنیم. چون ممکن است کاربری تصویری را حذف کند، اما بجای ذخیره سازی اطلاعات رکورد، بر روی دکمهی back کلیک کند. بنابراین در اینجا حذف تصاویر را صرفا به حذف آنها از UI محدود میکنیم و حذف نهایی را به زمان کلیک بر روی دکمهی ذخیره سازی اطلاعات در حال ویرایش، موکول خواهیم کرد.
به همین جهت در ابتدا با کلیک بر روی دکمهی حذف، ابتدا با حذف آن تصویر از HotelRoomImages، سبب به روز رسانی UI خواهیم شد، اما این تصویر را واقعا حذف نمیکنیم. در اینجا فقط نام آنرا در یک لیست، برای حذف نهایی، ذخیره سازی خواهیم کرد:
@code { private List<string> DeletedImageFileNames = new List<string>(); private void DeletePhoto(HotelRoomImageDTO imageDto) { var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase); if (HotelRoomModel.Id == 0 && Title == "Create") { // ... } else { // Edit Mode DeletedImageFileNames.Add(imageFileName); HotelRoomModel.HotelRoomImages.Remove(imageDto); // Update UI } }
سپس در جائیکه کار مدیریت ثبت اطلاعات صورت میگیرد، پس از به روز رسانی رکورد متناظر با یک اتاق، بر اساس لیست DeletedImageFileNames، فایلهای علامتگذاری شدهی برای حذف را نیز واقعا از سرور حذف میکنیم:
private async Task HandleHotelRoomUpsert() { // ... if (HotelRoomModel.Id != 0 && Title == "Update") { // Update Mode var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel); foreach(var imageFileName in DeletedImageFileNames) { FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder); } // await AddHotelRoomImageAsync(updatedRoomDto); await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully."); } else { // ... } } }
نمایش «لطفا منتظر بمانید» در حین آپلود تصاویر
در ادامه میخواهیم تا پایان نمایش آپلود تصاویر، پیام «لطفا منتظر بمانید» را به همراه یک spinner نمایش دهیم. بنابراین در ابتدا کلاسهای جدید زیر را به فایل wwwroot\css\site.css اضافه میکنیم:
.spinner { border: 16px solid silver !important; border-top: 16px solid #337ab7 !important; border-radius: 50% !important; width: 80px !important; height: 80px !important; animation: spin 700ms linear infinite !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%); position: absolute !important; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@code { private bool IsImageUploadProcessStarted; private async Task HandleImageUpload(InputFileChangeEventArgs args) { try { IsImageUploadProcessStarted = true; // ... } finally { IsImageUploadProcessStarted = false; } } }
<InputFile OnChange="HandleImageUpload" multiple></InputFile> <div class="row"> @if (IsImageUploadProcessStarted) { <div class="col-md-12"> <span><i class="spinner"></i> Please wait.. Images are uploading...</span> </div> }
دریافت تائیدیهی حذف، پس از کلیک بر روی دکمههای حذف تصاویر
در قسمت 12 این سری، کامپوننت Confirmation.razor را توسعه دادیم. در اینجا میخواهیم با کلیک بر روی دکمههای حذف تصاویر، ابتدا توسط این کامپوننت، تائیدیهای دریافت شود و در صورت تائید، آن تصویر انتخابی را حذف کنیم.
به همین جهت در ابتدا فایل Confirmation.razor را به پوشهی جدید Pages\Components کپی میکنیم. سپس فضای نام آنرا به فایل BlazorServer\BlazorServer.App\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامه قابل استفاده شود:
@using BlazorServer.App.Pages.Components
<Confirmation @ref="Confirmation1" OnCancel="OnCancelDeleteImageClicked" OnConfirm="@(()=>OnConfirmDeleteImageClicked(ImageToBeDeleted))"> <div> Do you want to delete @ImageToBeDeleted?.RoomImageUrl image? </div> </Confirmation>
- سپس روالهای رویدادگردان OnCancel و OnConfirm به متدهایی در کامپوننت جاری متصل شدهاند.
- در آخر پیامی تعریف شدهاست.
برای اینکه کامپوننت فوق عمل کند، نیاز است تغییرات زیر را به قسمت کدها اعمال کنیم:
private Confirmation Confirmation1; private HotelRoomImageDTO ImageToBeDeleted; private void OnCancelDeleteImageClicked() { // Confirmation1.Hide(); } private void DeletePhoto(HotelRoomImageDTO imageDto) { ImageToBeDeleted = imageDto; Confirmation1.Show(); } private void OnConfirmDeleteImageClicked(HotelRoomImageDTO imageDto) {
- در اینجا محتوای متد DeletePhoto اصلی را (متدی را که تا پیش از این مرحله تکمیل کردیم) به متد جدید OnConfirmDeleteImageClicked منتقل کردهایم. یعنی در ابتدا فقط یک modal نمایش داده میشود. پس از اینکه کاربر عملیات حذف را تائید کرد، رویداد OnConfirm، سبب فراخوانی متد OnConfirmDeleteImageClicked خواهد شد (که همان DeletePhoto قبل از این تغییرات است).
حذف کامل یک اتاق به همراه تمام تصاویر منتسب به آن
مرحلهی آخر این قسمت، اضافه کردن دکمهی حذف، به ردیفهای کامپوننت نمایش لیست اتاقها است که این مورد نیز باید به همراه دریافت تائیدیهی حذف و همچنین حذف تمام وابستگیهای اتاق ثبت شده باشد:
<td> <NavLink href="@($"hotel-room/edit/{room.Id}")" class="btn btn-primary">Edit</NavLink> <button class="btn btn-danger" @onclick="()=>HandleDeleteRoom(room)">Delete</button> </td>
اکنون برای مدیریت دریافت تائیدیهی حذف از کاربر، کامپوننت Confirmation را اضافه کرده:
<Confirmation @ref="Confirmation1" OnCancel="OnCancelDeleteRoomClicked" OnConfirm="OnConfirmDeleteRoomClicked"> <div> Do you want to delete @RoomToBeDeleted?.Name? </div> </Confirmation>
@code { private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>(); private HotelRoomDTO RoomToBeDeleted; private Confirmation Confirmation1; private void OnCancelDeleteRoomClicked() { // Confirmation1.Hide(); } private void HandleDeleteRoom(HotelRoomDTO roomDto) { RoomToBeDeleted = roomDto; Confirmation1.Show(); } private async Task OnConfirmDeleteRoomClicked() { if(RoomToBeDeleted is null) { return; } await HotelRoomService.DeleteHotelRoomAsync(RoomToBeDeleted.Id); HotelRooms.Remove(RoomToBeDeleted); // Update UI }
مشکل! این روش استفادهی از DbContext کار نمیکند!
اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم:
An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'. System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-18.zip
var ss = require("sdk/simple-storage"); ss.storage.myArray = [1, 1, 2, 3, 5, 8, 13]; ss.storage.myBoolean = true; ss.storage.myNull = null; ss.storage.myNumber = 3.1337; ss.storage.myObject = { a: "foo", b: { c: true }, d: null }; ss.storage.myString = "O frabjous day!";
delete ss.storage.value;
if (!ss.storage.Variables) { ss.storage.Variables=[]; ss.storage.Variables.push(true); ss.storage.Variables.push(false); ss.storage.Variables.push(false); ss.storage.Variables.push(false); } if (!ss.storage.interval) ss.storage.interval=1; if (!ss.storage.DateVariables) { var now=String(new Date()); ss.storage.DateVariables=[]; ss.storage.DateVariables.push(now); ss.storage.DateVariables.push(now); ss.storage.DateVariables.push(now); ss.storage.DateVariables.push(now); }
contentScriptFile: self.data.url("jquery.min.js") contentScriptFile: [self.data.url("jquery.min.js"),self.data.url("const.js"),self.data.url("popup.js")]
از شیء port به صورت عملی استفاده میکنیم. کد main.js را به صورت زیر تغییر دادیم:
function handleChange(state) { if (state.checked) { panel.show({ position: button }); var v1=[],v2; if (ss.storage.Variables) v1=ss.storage.Variables; if (ss.storage.interval) v2=ss.storage.interval; panel.port.emit("vars",v1,v2); } } panel.port.on("vars", function (vars,interval) { ss.storage.Variables=vars; ss.storage.interval=interval; });
$(document).ready(function () { addon.port.on("vars", function(vars,interval) { if (vars) { $("#chkarticles").attr("checked", vars[0]); $("#chkarticlescomments").attr("checked", vars[1]); $("#chkshares").attr("checked", vars[2]); $("#chksharescomments").attr("checked", vars[3]); } $("#interval").val(interval); }); $("#btnsave").click(function() { var Vposts = $("#chkarticles").is(':checked'); var VpostsComments = $("#chkarticlescomments").is(':checked'); var Vshares = $("#chkshares").is(':checked'); var VsharesComments = $("#chksharescomments").is(':checked'); var Vinterval = $("#interval").val() ; var Variables=[]; Variables[0]=Vposts; Variables[1]=VpostsComments; Variables[2]=Vshares; Variables[3]=VsharesComments; interval=Vinterval; addon.port.emit("vars", Variables,Vinterval); $("#messageboard").text( Messages.SettingsSaved); }); });
نکته بسیار مهم: در کد بالا ما فایل جاوااسکریت را از طریق فایل popup.html معرفی کردیم، نه از طریق خصوصیت contentscriptfile. این نکته را همیشه به خاطر داشته باشید. فایلهای js خود را تنها در دو حالت استفاده کنید:
- از طریق دادن رشته به خصوصیت contentScript و استفاده از self به جای addon
- معرفی فایل js داخل خود فایل html با تگ script که به درد اسکریپتهای با کد زیاد میخورد.
اگر فایل شما شامل استفاده از کلمهی کلیدی addon نمیشود، میتوانید فایل js خود را از طریق contentScriptFile هم اعمال کنید.
فایل popup.html
<script src="jquery.min.js"></script> <!-- Including jQuery --> <script type="text/javascript" src="const.js"></script> <script type="text/javascript" src="popup.js"></script>
<script type="text/javascript" src="const.js"></script> <script type="text/javascript" src="jquery.min.js"></script> <script type="text/javascript" src="https://www.google.com/jsapi"></script> <script type="text/javascript" src="rssreader.js"></script>
pageWorker = require("sdk/page-worker"); page= pageWorker.Page({ contentScriptWhen: "ready", contentURL: self.data.url("./background.htm") }); page.port.emit("vars",ss.storage.Variables,ss.storage.DateVariables,ss.storage.interval);
var variables=[]; var datevariables=[]; var period_time=60000; var timer; google.load("feeds", "1"); $(document).ready(function () { addon.port.on("vars", function(vars,datevars,interval) { if (vars) { Variables=vars; } if (datevars) { datevariables=datevars; } if(interval) period_time=interval*60000; alarmManager(); }); }); function alarmManager() { timer = setInterval(Run,period_time); } function Run() { if(Variables[0]){RssReader(Links.postUrl,0, Messages.PostsUpdated);} if(Variables[1]){RssReader(Links.posts_commentsUrl,1,Messages.CommentsUpdated); } if(Variables[2]){RssReader(Links.sharesUrl,2,Messages.SharesUpdated);} if(Variables[3]){RssReader(Links.shares_CommentsUrl,3,Messages.SharesCommentsUpdated);} } function RssReader(URL,index,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); var lastupdate=new Date(datevariables[index]); if(RssUpdate>lastupdate) { datevariables[index]=strRssUpdate; addon.port.emit("notification",datevariables,Message); } } }); }
- چه بخشهایی از سایت باید بررسی شوند.
- آخرین تاریخ تغییر هر کدام که در زمان نصب افزونه، تاریخ نصب افزونه میشود و با اولین به روز رسانی، تاریخ جدیدی جای آن را میگیرد.
- دورهی سیکل زمانی یا همان interval بر اساس دقیقه
Run
RSSReader
page.port.on("notification",function(lastupdate,Message) { ss.storage.DateVariables=lastupdate; Make_a_Notification(Message); }) function Make_a_Notification(Message) { var notifications = require("sdk/notifications"); notifications.notify({ title: "سایت به روز شد", text: Message, iconURL:self.data.url("./icon-64.png"), data:"https://www.dntips.ir", onClick: function (data) { tabs.open(data); } }); }
البته این نکته قابل ذکر است که اگر کاربر طلاعات پنل را به روزرسانی کند، تا وقتی که مرورگر بسته نشده و دوباره باز نشود تغییری نمیکند؛ چرا که ما تنها در ابتدای امر مقادیر ذخیره شده را به RSSReader فرستاده و اگر کاربر آنها را به روز کند، ارسال پیام دیگری توسط page worker صورت نمیگیرد. پس کد موجود در main.js را به صورت زیر ویرایش میکنیم:
pageWorker = require("sdk/page-worker"); page= pageWorker.Page({ contentScriptWhen: "ready", contentURL: self.data.url("./background.htm") }); function SendData() { page.port.emit("vars",ss.storage.Variables,ss.storage.DateVariables,ss.storage.interval); } SendData(); panel.port.on("vars", function (vars,interval) { ss.storage.Variables=vars; ss.storage.interval=interval; SendData(); });
var timer; function alarmManager() { timer = setInterval(Run,period_time); } addon.port.on("vars", function(vars,datevars,interval) { if (vars) { Variables=vars; } if (datevars) { datevariables=datevars; } if(interval) period_time=interval*60000; if(timer!=null) { clearInterval(timer); } alarmManager(); });
Page Mod
var pageMod = require("sdk/page-mod"); pageMod.PageMod({ include: "*.mozilla.org", contentScript: 'window.alert("Page matches ruleset");' });
var data = require("sdk/self").data; var pageMod = require("sdk/page-mod"); pageMod.PageMod({ include: "*.mozilla.org", contentScriptFile: [data.url("jquery-1.7.min.js"), data.url("my-script.js")] });
پنل تنظیمات
"preferences": [{ "description": "مطالب سایت", "type": "bool", "name": "post", "value": true, "title": "مطالب سایت" }, { "description": "نظرات مطالب سایت", "type": "bool", "name": "postcomments", "value": false, "title": "نظرات مطالب سایت" }, { "description": "اشتراک ها", "type": "bool", "name": "shares", "value": false, "title": "اشتراک ها" }, { "description": "نظرات اشتراک ها", "type": "bool", "name": "sharescomments", "value": false, "title": "نظرات اشتراک ها" }, { "description": "دوره زمان برای بررسی سایت", "name": "interval", "type": "integer", "value": 10, "title": "دوره زمانی" }]
از آنجا که مقادیر بالا تنها مقادیر پیش فرض خودمان هست و اگر کاربر آنها را تغییر دهد، در این صفحه هم باید اطلاعات تصحیح شوند، برای همین از کد زیر برای دسترسی به پنل تنظیمات و کنترلهای موجود آن استفاده میکنیم. همانطور که میبینید کد مورد نظر را در یک تابع به نام Perf_Default_Value قرار دادیم و آن را در بدو اجرا صدا زدیم. پس کاربر اگر به پنل تنظمیات رجوع کند، میتواند تغییراتی را که قبلا داده است، ببیند. بنابراین اگر الان تغییری را ایجاد کند، تا باز شدن مجدد مرورگر چیزی نمایش داده نمیشود. برای همین دقیقا مانند تابع SendData این تابع را هم در کد شنود پنل panel اضافه میکنیم؛ تا اگر کاربر اطلاعات را از طریق روش قبلی تغییر داد، اطلاعات هم اینک به روز شوند.
function Perf_Default_Value() { var preferences = require("sdk/simple-prefs").prefs; preferences.post = ss.storage.Variables[0]; preferences.postcomments = ss.storage.Variables[1]; preferences.shares = ss.storage.Variables[2]; preferences.sharescomments = ss.storage.Variables[3]; preferences["myinterval"] =parseInt(ss.storage.interval); } Perf_Default_Value(); panel.port.on("vars", function (vars,interval) { ss.storage.Variables=vars; ss.storage.interval=interval; SendData(); Perf_Default_Value(); });
perf=require("sdk/simple-prefs"); var preferences = perf.prefs; function onPrefChange(prefName) { switch(prefName) { case "post": ss.storage.Variables[0]=preferences[prefName]; break; case "postcomments": ss.storage.Variables[1]=preferences[prefName]; break; case "shares": ss.storage.Variables[2]=preferences[prefName]; break; case "sharescomments": ss.storage.Variables[3]=preferences[prefName]; break; case "myinterval": ss.storage.interval=preferences[prefName]; break; } } //perf.on("post", onPrefChange); //perf.on("postcomments", onPrefChange); perf.on("", onPrefChange);
اشکال زدایی Debug