نظرات مطالب
ساختار پروژه های Angular
ضمن تشکر فراوان از جناب آقای پاکدل عزیز، در این مقاله به خوبی درباره lazy loading در angularjs بحث شده. نکته مهم اینکه حتما پروژه‌ی قابل اجرایی که در انتهای مقاله لینک شده را ملاحظه کنید. نکاتی در این پروژه هست از جمله اینکه برای دسترسی به providerها برای lazy loading آنها به این ترتیب به app افزوده شده اند:
app.config([
        '$stateProvider',
        '$urlRouterProvider',
        '$locationProvider',
        '$controllerProvider',
        '$compileProvider',
        '$filterProvider',
        '$provide',


        function ($stateProvider, $urlRouterProvider, $locationProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
            //برای رجیستر کردن غیر همروند اجزای انگیولاری در آینده
            app.lazy =
            {
                controller: $controllerProvider.register,
                directive:  $compileProvider.directive,
                filter:     $filterProvider.register,
                factory:    $provide.factory,
                service:    $provide.service
            };
.
.
.
])
(البته این کد از پروژه خودمان است و بعضی وابستگی‌های دیگر هم تزریق شده‌اند).
استفاده از app.lazy باعث سهولت بیشتر در استفاده و خواناتر شدن کد می‌شود. در ادامه به این ترتیب می‌توانید از app.lazy استفاده کنید:
angular.module('app').lazy.controller('myController',
        ['$scope',  function($scope){
...
}]);
به این ترتیب کد نوشته شده به دلیل نام گذاری ارجاع controllerProvider  با  controller  به حالت عادی شبیه است، و از طرفی lazy پیش از آن به فهم ماجرا کمک خواهد کرد.
این نقطه شروع یکی از پروژه‌های ماست که به عنوان نمونه بد نیست ملاحظه کنید:
<script type="text/javascript">
// --- Scriptjs ---
!function (a, b, c) { function t(a, c) { var e = b.createElement("script"), f = j; e.onload = e.onerror = e[o] = function () { e[m] && !/^c|loade/.test(e[m]) || f || (e.onload = e[o] = null, f = 1, c()) }, e.async = 1, e.src = a, d.insertBefore(e, d.firstChild) } function q(a, b) { p(a, function (a) { return !b(a) }) } var d = b.getElementsByTagName("head")[0], e = {}, f = {}, g = {}, h = {}, i = "string", j = !1, k = "push", l = "DOMContentLoaded", m = "readyState", n = "addEventListener", o = "onreadystatechange", p = function (a, b) { for (var c = 0, d = a.length; c < d; ++c) if (!b(a[c])) return j; return 1 }; !b[m] && b[n] && (b[n](l, function r() { b.removeEventListener(l, r, j), b[m] = "complete" }, j), b[m] = "loading"); var s = function (a, b, d) { function o() { if (!--m) { e[l] = 1, j && j(); for (var a in g) p(a.split("|"), n) && !q(g[a], n) && (g[a] = []) } } function n(a) { return a.call ? a() : e[a] } a = a[k] ? a : [a]; var i = b && b.call, j = i ? b : d, l = i ? a.join("") : b, m = a.length; c(function () { q(a, function (a) { h[a] ? (l && (f[l] = 1), o()) : (h[a] = 1, l && (f[l] = 1), t(s.path ? s.path + a + ".js" : a, o)) }) }, 0); return s }; s.get = t, s.ready = function (a, b, c) { a = a[k] ? a : [a]; var d = []; !q(a, function (a) { e[a] || d[k](a) }) && p(a, function (a) { return e[a] }) ? b() : !function (a) { g[a] = g[a] || [], g[a][k](b), c && c(d) }(a.join("|")); return s }; var u = a.$script; s.noConflict = function () { a.$script = u; return this }, typeof module != "undefined" && module.exports ? module.exports = s : a.$script = s }(this, document, setTimeout)

$script(['/Scripts/Lib/jquery/jquery-1.10.2.min.js'], function () {
    $script(['/Scripts/Lib/angular/angular.js'], function () {
        $script(['/Scripts/Lib/angular/angular-ui-router.min.js',
                    '/Scripts/Lib/angular/angular-resource.min.js',
                    '/Scripts/Lib/angular/angular-cache.min.js',
                    '/Scripts/Lib/angular/angular-sanitize.min.js',
                    '/Scripts/Lib/angular/angular-animate.min.js',
                    '/Scripts/Lib/angular/angular-cookie.min.js',
                    '/APP/Common/directives.js'
                ], function () {
                    $script('/app/app.js', function () {
                        angular.bootstrap(document, ['app']);
                    });
                });
            })
        });
</script>

این تگ script در صفحه شروع پروژه آمده است.
کد minify شده scriptjs در ابتدا قرار دارد، پس از آن فایل‌های js مورد نیاز با رعایت وابستگی‌های احتمالی به ترتیب بارگذاری شده‌اند.
این قسمت resolve یکی از بخش‌های مسیریابی است: 
resolve: {
                        fileDeps: ['$q', '$rootScope', function ($q, $rootScope) {
                            var deferred = $q.defer();
                            var deps = ['/app/HotStories/dataContextService.js',
                                        '/app/HotStories/hotStController.js'];
                            $script(deps, function () {
                                $rootScope.$apply(function () {
                                    deferred.resolve();
                                });
                            });

                            return deferred.promise;
                        }]
                    }
این نحوه تعریف سرویسی که فایل آن در وابستگی‌ها آمده و قرار است lazy load شود:
angular.module('app').lazy.service('dataContextService',
        ['$rootScope', '$resource', '$angularCacheFactory', '$q', function($rootScope, $resource, $cacheFactory, $q){
...
}]);
و این هم نحوه تعریف کنترلری که فایل آن در وابستگی‌ها آمده و قرار است lazy load شود: 
angular.module('app').lazy.controller('hotStController',
        ['$scope', 'ipCookie', 'dataContextService', function($scope, ipCookie, dataContextService){
...
}]);


مطالب
Blazor 5x - قسمت 16 - کار با فرم‌ها - بخش 4 - تهیه سرویس‌های آپلود تصاویر
در ادامه می‌خواهیم برای هر اتاق ثبت شده، تعدادی تصویر مرتبط را نیز به سرور آپلود کرده و مشخصات آن‌ها را در بانک اطلاعاتی ثبت کنیم. به همین جهت در این قسمت سرویس ثبت اطلاعات تصاویر در بانک اطلاعاتی و سرویس آپلود فایل‌ها را تهیه می‌کنیم.


تعریف موجودیت و DbSet تصاویر یک اتاق هتل

برای اینکه بتوان اطلاعات تصاویر آپلودی را در بانک اطلاعاتی ثبت کرد، نیاز است یک رابطه‌ی یک به چند را بین یک اتاق و تصاویر مرتبط با آن برقرار کرد. به همین جهت ابتدا به پروژه‌ی BlazorServer.Entities.csproj مراجعه کرده و موجودیت ثبت اطلاعات تصاویر را تعریف می‌کنیم:
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class HotelRoomImage
    {
        public int Id { get; set; }

        public string RoomImageUrl { get; set; }

        [ForeignKey("RoomId")]
        public virtual HotelRoom HotelRoom { get; set; }
        public int RoomId { get; set; }
    }
}
که در اینجا باید سر دیگر این رابطه‌ی one-to-many، در جدول HotelRoom نیز تعریف شود:
namespace BlazorServer.Entities
{
    public class HotelRoom
    {
        // ...
        public virtual ICollection<HotelRoomImage> HotelRoomImages { get; set; }
    }
}
در آخر باید این موجودیت جدید را به Context برنامه معرفی کرد. برای اینکار به پروژه‌ی BlazorServer.DataAccess مراجعه کرده و DbSet متناظری را تعریف می‌کنیم:
namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<HotelRoomImage> HotelRoomImages { get; set; }

        // ...
    }
}
پس از این تغییرات، نیاز است یکبار دیگر عملیات Migrations را اجرا کرد، تا ساختار متناظر بانک اطلاعاتی این تغییرات ایجاد شود. بنابراین توسط خط فرمان به پوشه‌ی پروژه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم. در اینجا نگارش 5.0.3 باید معادل نگارشی از EF-Core باشد که از آن در حال استفاده‌اید:
dotnet tool update --global dotnet-ef --version 5.0.3
dotnet build
dotnet ef migrations --startup-project ../BlazorServer.App/ add Init --context ApplicationDbContext
dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
در مورد این دستورات در قسمت 13 بیشتر بحث شده‌است.


تعریف مدل UI متناظر با هر تصویر

همانطور که در قسمت 13 نیز عنوان شد، در حین کار با رابط کاربری برنامه، با موجودیت‌های بانک اطلاعاتی، به صورت مستقیم کار نخواهیم کرد و بر اساس نیازهای برنامه، یکسری کلاس DTO را تعریف می‌کنیم. بنابراین به پروژه‌ی BlazorServer.Models مراجعه کرده و DTO متناظر با HotelRoomImage را به صورت زیر اضافه می‌کنیم:
namespace BlazorServer.Models
{
    public class HotelRoomImageDTO
    {
        public int Id { get; set; }

        public int RoomId { get; set; }

        public string RoomImageUrl { get; set; }
    }
}
و همچنین جهت سهولت تبدیل اطلاعات بین موجودیت تعریف شده و DTO ی آن، نگاشت AutoMapper دو طرفه‌ای را در پروژه‌ی BlazorServer.Models.Mappings برقرار می‌کنیم:
using AutoMapper;
using BlazorServer.Entities;

namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            // ...
            CreateMap<HotelRoomImageDTO, HotelRoomImage>().ReverseMap(); // two-way mapping
        }
    }
}

تعریف سرویس کار با HotelRoomImage

در اینجا نیز همانند سرویسی که برای انجام عملیات تجاری مرتبط با یک اتاق هتل، در قسمت 13 پیاده سازی کردیم، سرویس دیگری را در پروژه‌ی BlazorServer.Services برای کار با تصاویر اتاق‌ها تهیه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IHotelRoomImageService
    {
        Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO);

        Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId);

        Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId);

        Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId);
    }
}
برای نمونه بر اساس اطلاعات مدل UI برنامه، نیاز است بتوانیم اطلاعات یک تصویر را ثبت و یا حذف کنیم و یا لیست تصاویر یک اتاق را از بانک اطلاعاتی دریافت کنیم؛ با این پیاده سازی:
namespace BlazorServer.Services
{
    public class HotelRoomImageService : IHotelRoomImageService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public HotelRoomImageService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO)
        {
            var image = _mapper.Map<HotelRoomImage>(imageDTO);
            await _dbContext.HotelRoomImages.AddAsync(image);
            return await _dbContext.SaveChangesAsync();
        }

        public async Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId)
        {
            var image = await _dbContext.HotelRoomImages.FindAsync(imageId);
            _dbContext.HotelRoomImages.Remove(image);
            return await _dbContext.SaveChangesAsync();
        }

        public async Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId)
        {
            var imageList = await _dbContext.HotelRoomImages.Where(x => x.RoomId == roomId).ToListAsync();
            _dbContext.HotelRoomImages.RemoveRange(imageList);
            return await _dbContext.SaveChangesAsync();
        }

        public Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId)
        {
            return _dbContext.HotelRoomImages
                            .Where(x => x.RoomId == roomId)
                            .ProjectTo<HotelRoomImageDTO>(_mapperConfiguration)
                            .ToListAsync();
        }
    }
}
پس از این تعاریف، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و این سرویس را به سیستم تزریق وابستگی‌های برنامه معرفی می‌کنیم:
namespace BlazorServer.App
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IHotelRoomImageService, HotelRoomImageService>();
            // ...


تهیه سرویسی برای آپلود فایل‌های یک برنامه‌ی Blazor Server به سرور

جهت ساده سازی کار آپلود، در برنامه‌های Blazor Server، سرویس جدید FileUploadService را به پروژه‌ی BlazorServer.Services اضافه می‌کنیم:
using Microsoft.AspNetCore.Components.Forms;
using System.Threading.Tasks;

namespace BlazorServer.Services
{
    public interface IFileUploadService
    {
        void DeleteFile(string fileName, string webRootPath, string uploadFolder);
        Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder);
    }
}
کار آن حذف یک فایل، بر اساس مسیر آن است و همچنین دریافت یک IBrowserFile از کاربر و ذخیره سازی اطلاعات آن در سرور؛ با این پیاده سازی:
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.IO;
using System.Threading.Tasks;

namespace BlazorServer.Services
{
    public class FileUploadService : IFileUploadService
    {
        private const int MaxBufferSize = 0x10000;

        public void DeleteFile(string fileName, string webRootPath, string uploadFolder)
        {
            var path = Path.Combine(webRootPath, uploadFolder, fileName);
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }

        public async Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder)
        {
            createUploadDir(webRootPath, uploadFolder);
            var (fileName, imageFilePath) = getOutputFileInfo(inputFile, webRootPath, uploadFolder);

            using (var outputFileStream = new FileStream(
                        imageFilePath, FileMode.Create, FileAccess.Write,
                        FileShare.None, MaxBufferSize, useAsync: true))
            {
                using var inputStream = inputFile.OpenReadStream();
                await inputStream.CopyToAsync(outputFileStream);
            }

            return $"{uploadFolder}/{fileName}";
        }

        private static (string FileName, string FilePath) getOutputFileInfo(
                    IBrowserFile inputFile, string webRootPath, string uploadFolder)
        {
            var fileName = Path.GetFileName(inputFile.Name);
            var imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName);
            if (File.Exists(imageFilePath))
            {
                var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
                var fileExtension = Path.GetExtension(fileName);
                fileName = $"{fileNameWithoutExtension}-{Guid.NewGuid()}{fileExtension}";
                imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName);
            }
            return (fileName, imageFilePath);
        }

        private static void createUploadDir(string webRootPath, string uploadFolder)
        {
            var folderDirectory = Path.Combine(webRootPath, uploadFolder);
            if (!Directory.Exists(folderDirectory))
            {
                Directory.CreateDirectory(folderDirectory);
            }
        }
    }
}
اگر در ASP.NET Core، اطلاعات فایل ارسالی به سرور، توسط IFormFile به اکشن متدهای کنترلرها ارسال می‌شود، در برنامه‌های Blazor Server اینکار توسط IBrowserFile صورت می‌گیرد. کلیات کار با آن، بسیار شبیه به IFormFile است و اگر به مطلب «بررسی روش آپلود فایل‌ها در ASP.NET Core» مراجعه کنید، تفاوت آنچنانی را مشاهده نخواهید کرد. تنها تفاوت پیاده سازی که در اینجا وجود دارد، نیاز به استفاده‌ی از متد ()inputFile.OpenReadStream جهت دسترسی به محتوای فایل آپلودی، برای ذخیره‌ی آن در سمت سرور است؛ وگرنه مابقی کدهای آپلود آن، با ASP.NET Core یکی است.
همچنین برای دسترسی به IBrowserFile در یک سرویس، نیاز است وابستگی زیر را نیز به پروژه‌ی سرویس‌ها اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.3" />
  </ItemGroup>
</Project>
پس از آن، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و این سرویس را به سیستم تزریق وابستگی‌های برنامه معرفی می‌کنیم:
namespace BlazorServer.App
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IFileUploadService, FileUploadService>();
            // ...
در قسمت بعد، از این سرویس‌ها جهت مدیریت آپلود تصاویر استفاده خواهیم کرد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-16.zip
مطالب
Blazor 5x - قسمت چهارم - مبانی Blazor - بخش 1 - Data Binding
عنوان می‌شود که HTML over Web socket آینده‌ی توسعه‌ی برنامه‌های وب است و این آینده هم اکنون توسط Blazor Server در دسترس است. در این مدل توسعه، ابتدا یک اتصال SignalR برقرار شده و سپس تمام تعاملات بین سرور و کلاینت، از طریق همین اتصال که عموما web socket است، مدیریت می‌شود. به همین جهت در ادامه قصد داریم یک پروژه‌ی Blazor Server را تکمیل کنیم. پس از آن یک پروژه‌ی Blazor WASM را نیز بررسی خواهیم کرد. بنابراین هر دو مدل توسعه‌ی برنامه‌های Blazor را پوشش خواهیم داد. برای این منظور در ابتدا مبانی Blazor را بررسی می‌کنیم که در هر دو مدل یکی است.


تعریف مدل برنامه

در همان پروژه‌ی خالی Blazor Server که در قسمت دوم با دستور dotnet new blazorserver ایجاد کردیم، پوشه‌ی Models را افزوده و کلاس BlazorRoom را در آن تعریف می‌کنیم:
namespace BlazorServerSample.Models
{
    public class BlazorRoom
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public decimal Price { set; get; }

        public bool IsActive { set; get; }
    }
}
سپس برای اینکه مدام نیاز به تعریف فضای نام آن در فایل‌های مختلف razor. برنامه نباشد، به فایل Imports.razor_ مراجعه کرده و سطر زیر را به انتهای آن اضافه می‌کنیم:
@using BlazorServerSample.Models
برنامه را نیز توسط دستور dotnet watch run اجرا می‌کنیم.


Data binding یک طرفه

در ادامه به فایل Pages\Index.razor مراجعه کرده و منهای سطر اول مسیریابی آن، مابقی محتوای آن‌را حذف می‌کنیم. در اینجا می‌خواهیم مقادیر نمونه‌ای از شیء BlazorRoom را نمایش دهیم. به همین جهت این شیء را در قسمت code@ فایل razor جاری (همانند نکات قسمت قبل)، ایجاد می‌کنیم:
@page "/"

<h2 class="bg-light border p-2">
    First Room
</h2>
Room: @Room.Name
<br/>
Price: @Room.Price

@code
{
    BlazorRoom Room = new BlazorRoom
    {
        Id = 1,
        Name = "Room 1",
        IsActive = true,
        Price = 499
    };
}
در اینجا در ابتدا شیء Room را در قسمت قطعه کد فایل razor جاری ایجاد کرده و سپس اطلاعات آن‌را با استفاده از زبان Razor نمایش داده‌ایم.


 به این روش نمایش اطلاعات، one-way data-binding نیز گفته می‌شود. اما چطور می‌توان یک طرفه بودن آن‌را متوجه شد؟ برای این منظور یک text-box را نیز در ذیل تعاریف فوق، به صورت زیر اضافه می‌کنیم که مقدارش را از Room.Price دریافت می‌کند:
<input type="number" value="@Room.Price" />
اکنون اگر این مقدار را تغییر دهیم، عدد جدید قیمت اتاق، به خاصیت Room.Price منعکس نمی‌شود و تغییری نمی‌کند:



Data binding دو طرفه

اکنون می‌خواهیم اگر مقدار ورودی Room.Price توسط text-box فوق تغییر کرد، نتیجه‌ی نهایی، به خاصیت متناظر با آن نیز اعمال شود و تغییر کند. برای این منظور فقط کافی است ویژگی value را به bind-value@ تغییر دهیم:
<input type="number" @bind-value="@Room.Price" />
ویژگی bind-value@ سبب برقراری data-binding دو طرفه می‌شود. یعنی در ابتدا مقدار اولیه‌ی خاصیت Room.Price را نمایش می‌دهد. در ادامه‌ی اگر کاربر، مقدار این text-box را تغییر داد، نتیجه‌ی نهایی را به خاصیت Room.Price نیز اعمال می‌کند و همچنین این تغییر، سبب به روز رسانی UI نیز می‌شود؛ یعنی در جائیکه پیشتر مقدار اولیه‌ی Room.Price را نمایش داده بودیم، اکنون مقدار جدید آن نمایش داده خواهد شد:


البته اگر برنامه را اجرا کنیم، با تغییر مقدار text-box، بلافاصله تغییری را مشاهده نخواهیم کرد. برای اعمال تغییرات نیاز خواهد بود تا در جائی خارج از text-box کلیک و focus را به المانی دیگر منتقل کنیم. اگر می‌خواهیم همراه با تایپ اطلاعات درون text-box، رابط کاربری نیز به روز شود، می‌توان bind-value را به یک رخداد خاص، مانند oninput متصل کرد. حالت پیش‌فرض آن onchange است:
<input type="number" @bind-value="@Room.Price" @bind-value:event="oninput" />
اکنون اگر برنامه را اجرا کرده و درون text-box اطلاعاتی را وارد کنیم، بلافاصله UI نیز به روز رسانی خواهد شد.
لیست کامل رخ‌دادها را در اینجا می‌توانید مشاهده کنید. برای مثال برای یک المان input، دو رخداد onchange و oninput قابل تعریف هستند.

یک نکته: در حین کار با bind-value@، نیازی نیست مقدار آن با @ شروع شود. یعنی ذکر "bind-value="Room.Price@ نیز کافی است.


تمرین 1 - خاصیت IsActive یک اتاق را به یک checkbox متصل کرده و همچنین وضعیت جاری آن‌را نیز در یک برچسب نمایش دهید.

در اینجا می‌خواهیم مقدار خاصیت Room.IsActive را توسط یک اتصال دو طرفه، به یک checkbox متصل کنیم:
<input type="checkbox" @bind-value="Room.IsActive"  />
<br/>
This room is @(Room.IsActive? "Active" : "Inactive").
با استفاده از bind-value@، وضعیت جاری خاصیت Room.IsActive را به یک checkbox متصل کرده‌ایم. همچنین در ادامه توسط یک عبارت شرطی، این وضعیت را نمایش داده‌ایم.


بار اولی که برنامه نمایش داده می‌شود، هر چند مقدار IsActive بر اساس مقدار دهی آن در شیء Room، مساوی true است، اما chekbox، علامت نخورده باقی می‌ماند. برای رفع این مشکل نیاز است ویژگی checked این المان را نیز به صورت زیر مقدار دهی کرد:
<input type="checkbox" @bind-value="Room.IsActive"
   checked="@(Room.IsActive? "cheked" : null)" />
در این حالت اگر اتاقی فعال باشد، مقدار ویژگی checked، به checked و در غیراینصورت به null تنظیم می‌شود. به این ترتیب مشکل عدم نمایش checkbox انتخاب شده در بار اول نمایش کامپوننت جاری، برطرف می‌شود.


اتصال خواص مدل‌ها به dropdown‌ها

اکنون می‌خواهیم مدل این مثال را کمی توسعه داده و خواص تو در تویی را به آن اضافه کنیم:
using System.Collections.Generic;

namespace BlazorServerSample.Models
{
    public class BlazorRoom
    {
        // ...

        public List<BlazorRoomProp> RoomProps { set; get; }
    }

    public class BlazorRoomProp
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public string Value { set; get; }
    }
}
برای مثال یک اتاق می‌تواند ویژگی‌هایی مانند مساحت، تعداد نفرات مجاز و غیره را داشته باشد. هدف از ویژگی جدید RoomProps، تعیین لیست این نوع موارد است.
پس از این تعاریف، فیلد Room را به صورت زیر به روز رسانی می‌کنیم تا تعدادی از خواص اتاق را به همراه داشته باشد:
@code
{
    BlazorRoom Room = new BlazorRoom
    {
        Id = 1,
        Name = "Room 1",
        IsActive = true,
        Price = 499,
        RoomProps = new List<BlazorRoomProp>
        {
            new BlazorRoomProp
            {
                Id = 1, Name = "Sq Ft", Value = "100"
            },
            new BlazorRoomProp
            {
                Id = 2, Name = "Occupancy", Value = "3"
            }
        }
    };
}
در ادامه می‌خواهیم این خواص را در یک dropdown نمایش دهیم. همچنین با انتخاب یک خاصیت از دراپ‌داون، مقدار خاصیت انتخابی را در یک برچسب نیز به صورت پویا نمایش خواهیم داد:
<select @bind="SelectedRoomPropValue">
    @foreach (var prop in Room.RoomProps)
    {
        <option value="@prop.Value">@prop.Name</option>
    }
</select>
<span>The value of the selected room prop is: @SelectedRoomPropValue</span>

@code
{
    string SelectedRoomPropValue = "";
    // ...
همانطور که مشاهده می‌کنید، انجام یک چنین کاری با Blazor بسیار ساده‌است و نیازی به استفاده از جاوا اسکریپت و یا جی‌کوئری ندارد.
در اینجا یک فیلد را در قطعه کد برنامه تعریف کرده و به المان select متصل کرده‌ایم. هرگاه آیتمی در این دراپ داون انتخاب شود، این فیلد، مقدار آن آیتم انتخابی را خواهد داشت. در ادامه توسط یک حلقه‌ی foreach، تمام خواص یک اتاق را دریافت کرده و به صورت options‌های یک select استاندارد، نمایش می‌دهیم. در آخر نیز مقدار SelectedRoomPropValue را نمایش داده‌ایم که این مقدار به صورت پویا تغییر می‌کند:



تعریف لیستی از اتاق‌ها

عموما در یک برنامه‌ی واقعی، با یک تک اتاق کار نمی‌کنیم. به همین جهت در ادامه لیستی از اتاق‌ها را تعریف و مقدار دهی اولیه خواهیم کرد:
@code
{
    string SelectedRoomPropValue = "";

    List<BlazorRoom> Rooms = new List<BlazorRoom>();

    protected override void OnInitialized()
    {
        base.OnInitialized();

        Rooms.Add(new BlazorRoom
        {
            Id = 1,
            Name = "Room 1",
            IsActive = true,
            Price = 499,
            RoomProps = new List<BlazorRoomProp>
            {
                new BlazorRoomProp
                {
                    Id = 1, Name = "Sq Ft", Value = "100"
                },
                new BlazorRoomProp
                {
                    Id = 2, Name = "Occupancy", Value = "3"
                }
            }
        });

        Rooms.Add(new BlazorRoom
        {
            Id = 2,
            Name = "Room 2",
            IsActive = true,
            Price = 399,
            RoomProps = new List<BlazorRoomProp>
            {
                new BlazorRoomProp
                {
                    Id = 1, Name = "Sq Ft", Value = "250"
                },
                new BlazorRoomProp
                {
                    Id = 2, Name = "Occupancy", Value = "4"
                }
            }
        });
    }
}
در ابتدا فیلد Rooms تعریف شده که لیستی از BlazorRoomها است. در ادامه بجای مقدار دهی مستقیم آن در همان سطح قطعه کد، آن‌را در یک متد life-cycle کامپوننت جاری به نام OnInitialized که مخصوص این نوع مقدار دهی‌های اولیه است، مقدار دهی کرده‌ایم.


نمایش لیست قابل ویرایش اتاق‌ها

اکنون می‌خواهیم به عنوان تمرین 2، لیست جزئیات اتاق‌های تعریف شده را نمایش دهیم؛ با این شرط که نام و قیمت هر اتاق، قابل ویرایش باشد. همچنین خواص تعریف شده نیز به صورت ستون‌هایی مجزا، نمایش داده شوند. برای مثال اگر دو خاصیت در اینجا تعریف شده، 2 ستون اضافه‌تر نیز برای نمایش آن‌ها وجود داشته باشد. به علاوه از آنجائیکه می‌خواهیم اتصال دوطرفه را نیز آزمایش کنیم، نام و قیمت هر اتاق را نیز در پایین جدول، مجددا به صورت برچسب‌هایی نمایش خواهیم داد.


برای رسیدن به تصویر فوق می‌توان به صورت زیر عمل کرد:
<div class="border p-2 mt-3">
    <h2 class="text-info">Rooms List</h2>
    <table class="table table-dark">
        @foreach(var room in Rooms)
        {
            <tr>
                <td>
                    <input type="text" @bind-value="room.Name" @bind-value:event="oninput"/>
                </td>
                <td>
                    <input type="text" @bind-value="room.Price" @bind-value:event="oninput"/>
                </td>
                @foreach (var roomProp in room.RoomProps)
                {
                    <td>
                        @roomProp.Name, @roomProp.Value
                    </td>
                }
            </tr>
        }
    </table>

    @foreach(var room in Rooms)
    {
        <p>@room.Name's price is @room.Price.</p>
    }
</div>
در اینجا یک حلقه‌ی تو در تو را مشاهده می‌کنید. حلقه‌ی بیرونی، ردیف‌های جدول را که شامل نام و قیمت هر اتاق است، به صورت input-boxهای متصل به خواص متناظر با آن‌ها نمایش می‌دهد. سپس برای اینکه بتوانیم خواص هر ردیف را نیز نمایش دهیم، حلقه‌ی دومی را بر روی room.RoomProps تشکیل داده‌ایم.
هدف از foreach پس از جدول، نمایش تغییرات انجام شده‌ی در input-boxها است. برای مثال اگر نام یک ردیف را تغییر دادیم، چون یک اتصال دو طرفه برقرار است، خاصیت متناظر با آن به روز رسانی شده و بلافاصله در برچسب‌های ذیل جدول، منعکس می‌شود.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-04.zip
مطالب
فراخوانی GraphQL API در یک کلاینت Angular
در قسمت‌های قبل ( ^  ، ^ و  ^ ) GraphQL  را در ASP.Net Core راه اندازی کردیم و در قسمت ( فراخوانی GraphQL API در یک کلاینت ASP.NET Core  ) از GraphQL API  فراهم شده در یک کلاینت ASP Net Core  استفاده کردیم. اکنون می‌خواهیم چگونگی استفاده از GraphQL را در انگیولار، یاد بگیریم.
Apollo Angular، به شما اجازه میدهد داده‌ها را از یک سرور GraphQL دریافت و از آن برای ساختن UI ‌های واکنشی و پیچیده در انگیولار استفاده کنید. وقتی که از Apollo Client استفاده می‌کنیم، نیازی نیست هیچ چیز خاصی را در مورد سینتکس query ‌ها یادبگیریم؛ به دلیل اینکه همه چیز همان استاندارد GraphQL می‌باشد. هر چیزی را که شما در GraphQL query IDE تایپ می‌کنید، می‌توانید آن‌ها را در کد‌های Apollo Client نیز قرار دهید.

Installation with Angular Schematics 
 
بعد از ایجاد یک پروژه انگیولار با دستور زیر
ng new apollo-angular-project
در ترمینال VS Code  دستور زیر را وارد نمایید: 
ng add apollo-angular
تنها کاری که بعد از اجرای دستور بالا نیاز است انجام دهیم، تنظیم آدرس سرور GraphQL می‌باشد. از این رو فایل graphql.module.ts را باز کنید و متغیر uri را مقدار دهی کنید.
const uri = 'https://localhost:5001/graphql';
اکنون همه چیز تمام شده‌است. شما می‌توانید اولین query خود را اجرا کنید. 

Installation without Angular Schematics 

اگر می‌خواهید Apollo را بدون کمک گرفتن از Angular Schematics نصب کنید، در ابتدا کتابخانه‌های زیر را نصب نمائید:
npm install --save apollo-angular \
  apollo-angular-link-http \
  apollo-link \
  apollo-client \
  apollo-cache-inmemory \
  graphql-tag \
  graphql
کتابخانه apollo-client نیازمند AsyncIterable می‌باشد. به همین جهت مطمئن شوید فایل tsconfig.json شما به صورت زیر است:
{
  "compilerOptions": {
    // ...
    "lib": [
      "es2017",
      "dom",
      "esnext.asynciterable"
    ]
  }
}
اکنون شما تمام وابستگی‌های مورد نیاز را دارید. سپس می‌توانید اولین Apollo Client خود را اجرا کنید. 
در ادامه، فایل app.module.ts را باز کرده و آن را مطابق زیر ویرایش کنید:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HttpClientModule } from "@angular/common/http";
import { ApolloModule, APOLLO_OPTIONS } from "apollo-angular";
import { HttpLinkModule, HttpLink } from "apollo-angular-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";


@NgModule({
  declarations:
    [
      AppComponent
    ],
  imports:
    [
      BrowserModule,
      HttpClientModule,
      ApolloModule,
      HttpLinkModule
    ],
  providers:
    [
      {
        provide: APOLLO_OPTIONS,
        useFactory: (httpLink: HttpLink) => {
          return {
            cache: new InMemoryCache(),
            link: httpLink.create({
              uri: "https://localhost:5001/graphql"
            })
          }
        },
        deps:
          [
            HttpLink
          ]
      }],
  bootstrap:
    [
      AppComponent
    ]
})
export class AppModule { }
چه کاری در اینجا انجام شده است:
- با استفاده از سرویس apollo-angular-link-http و HttpLink، کلاینت را به یک سرور GraphQL متصل می‌کنیم. 
- apollo-cache-inmemory و InMemoryCache محلی برای ذخیره سازی داده‌ها می‌باشد. 
- APOLLO_OPTIONS فراهم کننده تنظیمات Apollo Client است.
- HttpLink نیاز به HttpClient دارد. به همین خاطر است که ما از HttpClientModule استفاده کرده‌ایم. 

Links and Cache 
 Apollo Client، یک لایه واسط شبکه قابل تعویض را دارد که به شما اجازه می‌دهد تا تنظیم کنید که چگونه query ‌ها در HTTP ارسال شوند؛ یا کل بخش Network را با چیزی کاملا سفارشی سازی شده جایگزین کنید؛ مثل یک websocket transport.
apollo-angular-link-http : از Http  برای ارسال query ‌ها استفاده می‌کند. 
apollo-cache-inmemory : پیاده سازی کش پیش فرض برای Apollo Client 2.0 می‌باشد. 
 
نکته
 Apollo یک سرویس export شده انگیولار از apollo-angular، برای به اشتراک گذاشتن داده‌های GraphQL با UI شما می‌باشد.

شروع کار
به همان روش که فایل‌های Model را برای کلاینت ASP.NET Core ایجاد کردیم، در اینجا هم ایجاد می‌کنیم. کار را با ایجاد کردن یک پوشه جدید به نام types، شروع می‌کنیم و چند type را در آن تعریف خواهیم کرد ( OwnerInputType ،AccountType  و OwnerType ):
export type OwnerInputType = {
    name: string;
    address: string;
}
export type AccountType = {
    'id': string;
    'description': string;
    'ownerId' : string;
    'type': string;
}
import { AccountType } from './accountType';
 
export type OwnerType = {
    'id': string;
    'name': string;
    'address': string;
    'accounts': AccountType[];
}

سپس یک سرویس را به نام graphql ایجاد می‌کنیم: 
ng g s graphql
و آن را همانند زیر ویرایش می‌کنیم: 
import { Injectable } from '@angular/core';
import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';

@Injectable({
  providedIn: 'root'
})
export class GraphqlService {

  constructor(private apollo: Apollo) { }
  
}
اکنون همه چیز آماده است تا تعدادی query و mutation را اجرا کنیم ( providedIn ).

بازیابی تمامی Owner ها 
سرویس graphql  را باز می‌کنیم و آن را همانند زیر ویرایش می‌کنیم ( اضافه کردن متد getOwners ):
  public getOwners = () => {
    return this.apollo.query({
      query: gql`query getOwners{
      owners{
        id,
        name,
        address,
        accounts{
          id,
          description,
          type
        }
      }
    }`
    });
  }

سپس فایل app.component.ts را باز کرده و همانند زیر ویرایش می‌کنیم: 
export class AppComponent implements OnInit {

  public owners: OwnerType[];
  public loading = true;

  constructor(private graphQLService: GraphqlService) { }

  ngOnInit() {
    this.graphQLService.getOwners().subscribe(result => {
      this.owners = result.data["owners"] as OwnerType[];
      this.loading = result.loading;
    });
  }
}

و هم چنین app.component.html:
<div>
    <div *ngIf="!this.loading">
        <table>
            <thead>
                <tr>
                    <th>
                        #
                    </th>
                    <th>
                        نام و نام خانوادگی
                    </th>
                    <th>
                        آدرس
                    </th>
                </tr>
            </thead>

            <tbody>
                <ng-container *ngFor="let item of this.owners;let idx=index" [ngTemplateOutlet]="innertable"
                    [ngTemplateOutletContext]="{item:item, index:idx}"></ng-container>
            </tbody>

        </table>
    </div>
    <div *ngIf="this.loading">
        <p>
            در حال بارگذاری لیست ...
        </p>
    </div>
</div>


<ng-template #innertable let-item="item" let-idx="index">
    <tr>
        <td>{{idx+1}}</td>
        <td>{{item.name}}</td>
        <td>{{item.address}}</td>
    </tr>
    <tr *ngIf="this.item.accounts && this.item.accounts.length > 0">
        <td colspan="4">
            <div>
                <p>Accounts</p>
            </div>
            <div>
                <table>
                    <thead>
                        <tr>
                            <th>#</th>
                            <th>نوع</th>
                            <th>توضیحات</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr *ngFor="let innerItem of this.item.accounts;let innerIndex=index">
                            <td>
                                {{innerIndex+1}}
                            </td>
                            <td>
                                {{innerItem.type}}
                            </td>
                            <td>
                                {{innerItem.description}}
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </td>
    </tr>
</ng-template>
اکنون جهت اجرای پروژه، کد‌های ضمیمه شده در پایان قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را دانلود نمایید و آن را اجرا کنید: 
dotnet restore
dotnet run
سپس پروژه را اجرا کنید: 
ng serve

خروجی به صورت زیر می‌باشد (لیست تمامی Owner ‌ها به همراه Account‌های مربوط به هر Owner):


در متد getOwner، بجای apollo.query می‌توان از apollo.watchQuery استفاده کرد که در نمونه زیر، در ابتدا، GraphQL query را در تابع gql ( از graphql-tag ) برای خصوصیت query در متد apollo.watchQuery پاس میدهیم.
  public getOwners = () => {
    return this.apollo.watchQuery<any>({
      query: gql`query getOwners{
        owners{
          id,
          name,
          address,
          accounts{
            id,
            description,
            type
          }
        }
      }`
    })
  }

و سپس در کامپوننت:
export class AppComponent implements OnInit {

  loading: boolean;
  public owners: OwnerType[];

  private querySubscription: Subscription;

  constructor(private graphQLService: GraphqlService) { }

  ngOnInit() {
    this.querySubscription = this.graphQLService.getOwners()
      .valueChanges
      .subscribe(result => {
        this.loading = result.loading;
        this.owners = result.data["owners"] as OwnerType[];
      });
  }

  ngOnDestroy() {
    this.querySubscription.unsubscribe();
  }
}
متد watchQuery، شیء QueryRef را برگشت می‌دهد که خصوصیت valueChanges را دارد که آن یک Observable می‌باشد. Observable تنها یک بار صادر خواهد شد و آن زمانی است که query کامل می‌شود و خصوصیت loading به false  تنظیم خواهد شد؛ مگر اینکه شما پارامتر notifyOnNetworkStatusChange را در watchQuery  به true  تنظیم کنید. شیء منتقل شده از طریق یک Observable، شامل خصوصیات loading ،error و data است. هنگامی که نتیجه query برگشت داده می‌شود، به خصوصیت data انتساب داده می‌شود. 
 


مابقی query ‌ها و mutation ها از سرویس graphql
بازیابی یک Owner  مشخص 

  public getOwner = (id) => {
    return this.apollo.query({
      query: gql`query getOwner($ownerID: ID!){
      owner(ownerId: $ownerID){
        id,
        name,
        address,
        accounts{
          id,
          description,
          type
        }
      }
    }`,
      variables: { ownerID: id }
    })
  }

ایجاد یک Owner جدید 
  public createOwner = (ownerToCreate: OwnerInputType) => {
    return this.apollo.mutate({
      mutation: gql`mutation($owner: ownerInput!){
        createOwner(owner: $owner){
          id,
          name,
          address
        }
      }`,
      variables: { owner: ownerToCreate }
    })
  }

ویرایش یک Owner 
  public updateOwner = (ownerToUpdate: OwnerInputType, id: string) => {
    return this.apollo.mutate({
      mutation: gql`mutation($owner: ownerInput!, $ownerId: ID!){
        updateOwner(owner: $owner, ownerId: $ownerId){
          id,
          name,
          address
        }
      }`,
      variables: { owner: ownerToUpdate, ownerId: id }
    })
  }

و در نهایت حذف یک Owner
  public deleteOwner = (id: string) => {
    return this.apollo.mutate({
      mutation: gql`mutation($ownerId: ID!){
        deleteOwner(ownerId: $ownerId)
       }`,
      variables: { ownerId: id }
    })
  }
اکنون شما می‌توانید با ویرایش کردن فایل app.component.ts یا ایجاد کردن کامپوننت‌های جدید، از نتایج بدست آمده استفاده کنید.


کد‌های کامل این قسمت را از ایجا دریافت کنید : GraphQL_Angular.zip
کد‌های کامل قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را از اینجا دریافت کنید :  ASPCoreGraphQL_3.rar  
مطالب
قابلیت های کاربردی ASP.NET WebFroms - قسمت اول
قابلیت CompositeScript 
پس از گذشت مدتی که از توسعه پروژه مورد نظرتان می‌گذرد احتمالاً فایل‌های javascript زیادی در پروژه شما استفاده می‌شود که هم مدیریت و هم بار سنگینی بر سرعت بارگذاری اولیه سایت شما به دلیل زمان بارگذاری فایل‌های javascript خواهد گذاشت. در ASP.NET چندین روش برای مدیریت فایل‌های javascript , css  وجود دارد.
ساده‌ترین روش استفاده از امکانات خود ASP.Net  است. با قابلیت CompositeScript براحتی می‌توانید فایل‌های javascript خود را با هم ادغام و در یک فایل ScriptResource.axd برای Client ارسال نمایید. برای این کار کافیست از تگ جدید CompositeScript در کنترل ScriptManager استفاده نمایید. در کد زیر این قابلیت نمایش داده شده است:
 <asp:ScriptManager ID="ScriptManager" runat="server" ScriptMode="Release">
                <CompositeScript>
                    <Scripts>
                        <asp:ScriptReference Path="~/Scripts/jquery-1.7.2.min.js" />
                        <asp:ScriptReference Path="~/Scripts/jquery.ui.core.min.js" />
                        <asp:ScriptReference Path="~/Scripts/jquery.ui.widget.min.js" />
                        <asp:ScriptReference Path="~/Scripts/jquery.ui.mouse.min.js" />
                        <asp:ScriptReference Path="~/Scripts/jquery.ui.sortable.min.js" />
                    </Scripts>
                </CompositeScript>
 </asp:ScriptManager>

علاوه بر آن حتما خاصیت ScriptMode کنترل ScriptManager  را بر روی Release تنظیم نمایید تا از حداکثر کارایی و کش فایل‌ها استفاده نمایید

دسترسی به ScriptManager در صفحات دیگر و الصاق فایل‌های خاص

برخی از فایل‌های Javascript فقط در صفحات خاصی استفاده شده اند و لازم نیست در هر صفحه بارگذاری شود برای این کار کافی است فایل مورد نظر را در صفحه خاص به  ScriptManager اضافه نمایید. البته ابتدا لازم است به ScriptManager دسترسی داشته باشیم. کد زیر نحوه دسترسی به آن را نمایش داده است:

ScriptManager scriptManager = ScriptManager.GetCurrent(this.Page);
scriptManager.CompositeScript.Scripts.Add(new ScriptReference("~/Scripts/jquery.json.min.js"));
ادامه دارد...
مطالب
فایل‌های chm و مشکل فارسی - قسمت اول

همانطور که مطلع هستید از ویندوز ویستا به بعد، فرمت قدیمی فایل‌های راهنمای ویندوز (فایل‌های hlp) منسوخ شده تلقی می‌شود و فرمت پیشنهادی، chm است. نرم افزارهای زیادی برای تهیه فایل‌های compiled html help یا همان chm های معروف وجود دارند که معروفترین آن‌ها برنامه‌ی Help & Manual است.
اما تمام این برنامه‌ها در حقیقت پوسته‌ای هستند برای برنامه‌ی رایگان html help work shop مایکروسافت و در نهایت از کامپایلر آن استفاده می‌کنند. بنابراین چرا از برنامه‌ی رایگان اصلی استفاده نشود؟

اگر تا به حال با html help work shop کار نکرده‌اید، در ادامه مروری سریع بر آن خواهیم داشت:

الف) درست کردن فایل‌های صفحات راهنما
برنامه‌ی Help & Manual ایی که معرفی شد و تمام نمونه‌های مشابه آن، تنها کار مهمی را که انجام می‌دهند این است که شما را از یک html editor بی‌نیاز می‌کنند. بنابراین زمانیکه می‌خواهیم از برنامه‌ی اصلی html help work shop استفاده کنیم نیاز به یک html editor خارجی برای تهیه فایل‌های راهنما خواهیم داشت. مثلا مرحوم front page یا نگارش جدید آن به نام Microsoft expression web یا Dreamweaver یا Aptana یا حتی notepad !
در اینجا تنها مشخص کردن نوع encoding نمایش صفحه برای صحیح نمایش داده شدن متون فارسی کافی است. اما این تمام ماجرا نیست.

ب) کامپایل کردن فایل‌های راهنمای ایجاد شده
برای این منظور بجای آپلود بیش از 30 تصویر جهت توضیحات قدم به قدم نحوه‌ی انجام این‌کار، یک فایل ویدیویی درست کرده‌ام که آن‌‌را از آدرس زیر می‌توانید دریافت کنید.
دریافت فایل

همانطور که در این فایل آموزشی مقدماتی هم تاکید شده، به صورت پیش فرض جستجوی فارسی کار نمی‌کند. همچنین اگر یک متن انگلیسی را جستجو کنید، صفحات یاد شده با عنوانی به هم ریخته نمایش داده می‌شوند. در صفحه افزودن به علاقمندی‌ها نیز یک چنین مشکلی با عنوان‌ها وجود دارد.

در قسمت دوم، نحوه‌ی رفع این دو مشکل (مشکل جستجوی عبارات فارسی و مشکل عنوان‌های به هم ریخته) را بررسی خواهیم کرد.

ادامه دارد ...

راهنماهای پروژه‌ها
چگونه فایل‌های PDF را نمایش دهیم؟
بهترین نمایشگر فایل‌های PDF بدون شک نرم افزار رایگان Adobe Reader است. پس از نصب این برنامه، Active-X همراه آن در انواع و اقسام برنامه‌ها و حتی صفحات وب نیز قابل استفاده است. برای نمونه به دو مطلب ذیل در سایت مراجعه نمائید:
استفاده از کنترل‌های Active-X در WPF  
نمایش یک فایل PDF در WinForms ، WPF و سیلورلایت

همچنین روش‌های دیگری که عموما مبتنی بر تبدیل PDF به تصویر هستند نیز جهت نمایش فایل‌های PDF کاربرد دارند. در لینک ذیل، جمع آوری مطالب مرتبطی را خواهید یافت:
iTextSharp

به علاوه مرورگرهای مدرن نیز در پی ارائه پشتیبانی توکاری از نمایش فایل‌های PDF هستند. برای نمونه می‌توان به پروژه PDF.js فایرفاکس که از نگارش 15 آن جرئی از فایرفاکس شده است، اشاره کرد.
   

بازخوردهای دوره
بوت استرپ (نگارش 3) چیست؟
سلام
میخواستم بدونم چرا به جای خواندن جی کوئری از سایت آن
 < script src = "http://code.jquery.com/jquery-latest.min.js" ></ script >
اون رو هم مثل بقیه فایل‌های جاوا اسکریپت دانلود نمیکنیم تا به صورت local استفاده کنیم؟
 < script src = "Scripts/jquery-latest.min.js  " ></ script >
بازخوردهای دوره
بوت استرپ (نگارش 3) چیست؟
- همانطور که عنوان شد، در سایت اصلی دریافت بوت استرپ، امکان سفارشی سازی و کار پویا با نگارش LESS آن وجود دارد. برای مثال قسمت تغییر پویای پیش فرض‌های navbar در اینجا
- اگر خودتان مستقیما می‌خواهید این موارد را تغییر دهید، کلیه فایل‌های LESS آن در مخزن اصلی کدهای بوت استرپ موجود هستند. در اینجا . برای نمونه فایل navbar.less آن.
اگر به فایل‌های less آن دقت کنید، تمام آن‌ها پارامتری هستند و هیچکدام دارای مقدار پیش فرضی نیستند. مقادیر پیش فرض مورد استفاده در فایل variables.less تعریف شده‌اند. برای تغییر کلی فایل CSS نهایی فقط کافی است که مقادیر این فایل را تغییر دهید.
فایل نهایی که سبب کامپایل تمام تغییرات خواهد شد، فایل bootstrap.less است.
- استفاده از فایل‌های LESS در نگارش‌های جدید VS.NET ساده‌است و به صورت توکار و خودکار، تغییرات را کامپایل کرده و فایل CSS نهایی را تولید می‌کند:


روی فایل bootstrap.less دوبار کلیک کنید (تمام فایل‌های پوشه less سورس بوت استرپ به VS.NET اضافه شده‌اند). اندکی صبر کنید تا کار کامپایل تمام شود. در صفحه‌ای که باز می‌شود، دو ستون قابل مشاهده است. ستون سمت چپ، فایل less اصلی است و ستون سمت راست، خروجی CSS کامپایل شده نهایی.
بازخوردهای دوره
صفحات مودال در بوت استرپ 3
فایل layout مثال پیوست شده را بررسی کنید. در آن لیست اسکریپت‌های مورد نیاز و همچنین ترتیب دقیق آ‌ن‌ها ذکر شده. برای مثال اول باید jQuery باشد و بعد اسکریپت jquery.validate و الی آخر. فایل‌های مربوطه هم در پوشه‌ی Scripts پروژه پیوست شده موجود است.