مطالب
Minimal API's در دات نت 6 - قسمت پنجم - پیاده سازی الگوی CQRS
تا قسمت قبل موفق شدیم فایل Program.cs برنامه‌ی Minimal API's را خلوت کنیم و همچنین زیرساختی را برای توسعه‌ی مبتنی بر ویژگی‌ها، ارائه دهیم. اما ... هنوز endpoints ما چنین شکلی را دارند:
        endpoints.MapGet("/api/authors", async (MinimalBlogDbContext ctx) =>
        {
            var authors = await ctx.Authors.ToListAsync();
            return authors;
        });

        endpoints.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) =>
        {
            var author = new Author();
            author.FirstName = authorDto.FirstName;
            author.LastName = authorDto.LastName;
            author.Bio = authorDto.Bio;
            author.DateOfBirth = authorDto.DateOfBirth;

            ctx.Authors.Add(author);
            await ctx.SaveChangesAsync();

            return author;
        });
 و یک چنین رویه‌ای جهت کار مستقیم با DbContext در اکشن متدهای MVC هیچگاه توصیه نمی‌شود. برای مثال به طور معمول، عملیاتی که در بدنه‌ی Lambda expressions فوق انجام شده، عموما به Repositories و Services محول شده و در نهایت از سرویس‌ها، در اکشن متدها استفاده می‌شود. در معماری جاری که در پیش گرفته‌ایم، دو لایه‌ی Repositories و Services حذف شده‌اند و دیگر خبری از آن‌ها نیست. در اینجا کار سرویس‌ها و مخازن، به هندلرهای معماری CQRS واگذار خواهند شد. هر هندلر نیز متکی به خود است و مستقل از سایر هندلرها طراحی می‌شود و این‌ها صرفا بر اساس نیازهای ویژگی جاری توسعه خواهند یافت و دقیقا در همان پوشه‌ی ویژگی مورد بررسی نیز قرار می‌گیرند؛ و نه پراکنده در لایه‌ای و یا پروژه‌ای دیگر. به این ترتیب درک یک ویژگی متکی به خود برنامه، ساده‌تر شده و در طول زمان، نگهداری و توسعه‌ی آن نیز ساده‌تر خواهد شد. مشکل داشتن سرویس‌هایی بزرگ که در معماری‌های متداول وجود دارند، استفاده‌ی از متدهای آن‌ها در چندین اکشن متد و چندین کنترلر مختلف است و اگر یکی از متدهای این سرویس بزرگ ما تغییر کند، بر روی چندین کنترلر تاثیر می‌گذارد که ممکن است سبب از کار افتادگی بعضی از آن‌ها شود؛ اما در اینجا هرکاری که انجام می‌شود و هر هندلری که توسعه می‌یابد، فقط مختص به یک کار و یک ویژگی مشخص است.


ایجاد Command و هندلر مخصوص ایجاد یک نویسنده‌ی جدید


در الگوی CQRS، یک دستور، کاری را بر روی بانک اطلاعاتی انجام می‌دهد. برای مثال در اینجا قرار است نویسنده‌ای را ثبت کند. در ادامه می‌خواهیم بدنه‌ی endpoints.MapPost فوق را با الگوی CQRS انطباق دهیم. به همین جهت به یک Command نیاز داریم:
using MediatR;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Api.Features.Authors;

public class CreateAuthorCommand : IRequest<Author>
{
    public AuthorDto AuthorDto { get; set; } = default!;
}
اینترفیس IRequest کتابخانه‌ی MediatR که در انتهای قسمت قبل به پروژه اضافه شد، چنین امضایی را دارد:
public interface IRequest<out TResponse> : IBaseRequest
یعنی <IRequest<Author به این معنا است که قرار است «خروجی» این عملیات، یک Author باشد و CreateAuthorCommand می‌تواند شامل تمام خواصی باشد که در جهت برآورده کردن این دستور مورد نیاز هستند؛ برای مثال کل اطلاعات شیء AuthorDto در اینجا.

سپس نیاز به یک هندلر است تا دستور رسیده را پردازش کند:
namespace MinimalBlog.Api.Features.Authors;

public class CreateAuthorCommandHandler : IRequestHandler<CreateAuthorCommand, Author>
{
    private readonly MinimalBlogDbContext _context;
    private readonly IMapper _mapper;

    public CreateAuthorCommandHandler(MinimalBlogDbContext context, IMapper mapper)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    public async Task<Author> Handle(CreateAuthorCommand request, CancellationToken cancellationToken)
    {
        if (request == null)
        {
            throw new ArgumentNullException(nameof(request));
        }

        var toAdd = _mapper.Map<Author>(request.AuthorDto);
        _context.Authors.Add(toAdd);
        await _context.SaveChangesAsync(cancellationToken);
        return toAdd;
    }
}
اینترفیس IRequestHandler چنین امضایی را دارد:
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
که اولین آرگومان جنریک آن، همان Command ای است که قرار است پردازش کند و خروجی آن، اطلاعاتی است که قرار است بازگشت دهد. یعنی متد Handle فوق، قرار است عملیات endpoints.MapPost را پیاده سازی کند و در اینجا با استفاده از AutoMapper، انتساب‌های آن حذف و ساده شده‌اند و مابقی آن، با بدنه‌ی lambda expression مربوط به endpoints.MapPost، یکی است. این هندلر، معادل یک یا چند متد از متدهای یک سرویس بزرگ است که در اینجا به صورت اختصاصی جهت پردازش فرمانی در کنار هم قرار می‌گیرند و متکی به خود هستند.

پس از این تغییرات، بدنه‌ی lambda expression مربوط به endpoints.MapPost به صورت زیر تغییر کرده و ساده می‌شود:
endpoints.MapPost("/api/authors", async (IMediator mediator, AuthorDto authorDto) =>
{
     var command = new CreateAuthorCommand { AuthorDto = authorDto };
     var author = await mediator.Send(command);
     return author;
});
در اینجا تزریق وابستگی IMediator را مشاهده می‌کنید. با فراخوانی متد Send آن، شیء‌ای به هندلر متناظری ارسال شده، پردازش می‌شود و در نهایت شیءای را بازگشت خواهد داد. برای مثال در اینجا شیء Dto یک نویسنده به هندلر CreateAuthorCommandHandler ارسال و تبدیل به شیءای از نوع Author مربوط به دومین برنامه شده، سپس در بانک اطلاعاتی ذخیره می‌شود و در نهایت این نویسنده که اکنون به همراه یک Id نیز هست، بازگشت داده می‌شود. بنابراین هر هندلر یک object in و یک object out دارد که به عنوان آرگومان‌های جنریک IRequestHandler تعریف می‌شوند.



نکته 1: await داخل بدنه‌ی lambda expression مربوط به endpoints را فراموش نکنید. تمام متدهای IMediator از نوع aysnc هستند؛ هرچند روش نامگذاری SendAsync را رعایت نکرده‌اند و اگر این await فراموش شود، مشاهده خواهید کرد که برنامه در حین فراخوانی endpoints در مرورگر، در حالت هنگ و صبر کردن نامحدود قرار می‌گیرد، بدون اینکه کاری را انجام دهد و یا حتی استثنایی را صادر کند.


نکته 2: در پیاده سازی هندلر، استفاده از cancellationToken را نیز مشاهده می‌کنید. تقریبا تمام متدهای async مربوط به EF-Core به همراه پارامتری جهت دریافت cancellationToken هم هستند. اگر کاربری قصد لغو یک درخواست طولانی را داشته باشد و بر روی دکمه‌ی stop مرورگر کلیک کند و یا حتی صفحه را چندین بار ریفرش کند، این به معنای abort درخواست(های) رسیده‌است. وجود این cancellationTokenها، بار سرور را کاهش داده و عملیات در حال اجرای سمت سرور را در یک چنین حالت‌هایی متوقف می‌کند.
البته هندلری که در اینجا تعریف شده، این cancellationToken را باید از mediator دریافت کند که در کدهای endpoint فوق، چنین نیست. برای رفع این مشکل باید به صورت زیر عمل کرد:
endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) =>
        {
            var request = new GetAllAuthorsQuery();
            var authors = await mediator.Send(request, ct);
            return authors;
        });
این مورد را می‌توان به صورت یک best practice، به تمام endpoints اضافه کرد.


نکته 3: هندلرها عموما چیزی را بازگشت نمی‌دهند؛ صرف نظر از هندلر فوق که نیاز بوده تا Id شیء ذخیره شده را بازگشت دهد، عموما به همراه هیچ خروجی نیستند. به همین جهت در حین تعریف آن‌ها فقط کافی است در آرگومان‌های جنریک آن‌ها، نوع خروجی را ذکر نکنیم:
public class Handler : IRequestHandler<Command>
در یک چنین حالتی، امضای IRequestHandler به صورت خودکار به همراه خروجی از نوع Unit خواهد بود:
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, Unit> where TRequest : IRequest<Unit>
که این Unit معادل Void در کتابخانه‌ی mediator است و به نحو زیر در هندلرها مدیریت می‌شود:
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
   // ...
   return Unit.Value;
}
در یک چنین حالتی، تعریف یک Command نیز بر اساس اینترفیس IRequest انجام می‌شود:
public class Command : IRequest


ایجاد Query و هندلر مخصوص بازگشت لیست نویسنده‌‌ها

در الگوی CQRS، یک کوئری قرار است اطلاعاتی را بازگشت دهد و ... وضعیت بانک اطلاعاتی را تغییر نمی‌دهد. بنابراین در اینجا یک IRequest که قرار است لیستی از نویسندگان را بازگشت دهد، تعریف می‌کنیم. بدنه‌ی آن هم می‌تواند خالی باشد و یا به همراه خواصی مانند اطلاعات صفحه بندی و یا مرتب سازی گزارشگیری رسیده‌ی از درخواست:
using MediatR;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Api.Features.Authors;

public class GetAllAuthorsQuery : IRequest<List<Author>>
{
}
سپس نیاز به یک هندلر است تا درخواست رسیده را پردازش کند. این هندلر، کوئری فوق را دریافت کرده و لیست کاربران را بازگشت می‌دهد:
namespace MinimalBlog.Api.Features.Authors;

public class GetAllAuthorsHandler : IRequestHandler<GetAllAuthorsQuery, List<Author>>
{
    private readonly MinimalBlogDbContext _context;

    public GetAllAuthorsHandler(MinimalBlogDbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public Task<List<Author>> Handle(GetAllAuthorsQuery request, CancellationToken cancellationToken)
    {
        return _context.Authors.ToListAsync(cancellationToken);
    }
}
پس از این تغییرات، بدنه‌ی lambda expression مربوط به endpoints.MapGet به صورت زیر تغییر کرده و ساده می‌شود:
endpoints.MapGet("/api/authors", async (IMediator mediator) =>
{
   var request = new GetAllAuthorsQuery();
   var authors = await mediator.Send(request);
   return authors;
});
مزیت استفاده‌ی از الگوی CQRS، تنها به حذف لایه‌ی سرویس و رسیدن به ویژگی‌هایی مستقل و متکی به خود، منحصر نیست. با استفاده از این الگو می‌توان مقیاس پذیری برنامه را نیز افزایش داد. برای مثال یک بانک اطلاعاتی بهینه سازی شده را صرفا برای کوئری‌ها، درنظر گرفت و بانک اطلاعاتی دیگری را تنها برای اعمال Write که Commands بر روی آن اجرا می‌شوند و در اینجا تنها نیاز به همگام سازی اطلاعات بانک اطلاعاتی Write، با بانک اطلاعاتی Read است که در بسیاری از اوقات پرکارتر از بانک‌های اطلاعاتی دیگر است:


و یا حتی معماری CQRS با معماری Event store نیز قابل ترکیب است:


در اینجا بجای استفاده از بانک اطلاعاتی Write، از یک Event store استفاده می‌شود. کار event store، دریافت رویدادهای write است و سپس باز پخش آن‌ها به بانک اطلاعاتی Read؛ تا کار همگام سازی به این نحو صورت گیرد.


روشی برای نظم دادن به نحوه‌ی تعریف کلاس‌های الگوی CQRS

تا اینجا برای مثال کلاسCreateAuthorCommand  را در یک فایل مجزا و سپس هندلر آن‌را به نام CreateAuthorCommandHandler در یک فایل دیگر تعریف کردیم. می‌توان جهت بالابردن خوانایی برنامه، کاهش رفت و برگشت‌ها برای یافتن کلاس‌های مرتبط و همچنین سهولت یافتن هندلرهای مرتبط با هر متد mediator.Send، از روش زیر نیز استفاده کرد:
public static class CreateAuthor
{
    public class Command : IRequest<AuthorGetDto>
    {
        // ...
    }

    public class Handler : IRequestHandler<Command, AuthorGetDto>
    {
       // ...
    }
}
در اینجا از nested classes استفاده شده‌است. ابتدا نام اصلی Command و یا کوئری ذکر می‌شود؛ که نام کلاس دربرگیرنده‌ی اصلی را تشکیل می‌دهد. سپس دو کلاس بعدی فقط Command و Handler نام می‌گیرند و نه هیچ نام دیگری. به این ترتیب به یکسری نام یک دست در کل پروژه خواهیم رسید. زمانیکه قرار است mediator.Send فراخوانی شود، اینبار چنین شکلی را پیدا می‌کند که مزیت آن، سهولت یافتن هندلر مرتبط، فقط با پیگیری کلاس اصلی CreateAuthor است:
var command = new CreateAuthor.Command { AuthorDto = authorDto };
var author = await mediator.Send(command, ct);

در مورد کوئری‌ها هم می‌توان به قالب مشابهی رسید که در اینجا هم کوئری و هندلر آن، ذیل نام اصلی مدنظر قرار می‌گیرند:
public static class GetAllAuthors
{
    public class Query : IRequest<List<AuthorGetDto>>
    {
       //...
    }

    public class Handler : IRequestHandler<Query, List<AuthorGetDto>>
    {
       //...
    }
}
و اگر کدهای نهایی این سری را که از قسمت اول قابل دریافت است بررسی کنید، از همین ساختار یکدست، برای تعاریف دستورات و کوئری‌ها استفاده شده‌است.
نظرات مطالب
C# 8.0 - Nullable Reference Types
یک نکته‌ی تکمیلی: تاثیر نوع‌های ارجاعی نال نپذیر C# 8.0 بر روی EF Core 3.0

تغییرات نحوه‌ی تعریف موجودیت‌ها در C# 8.0

تا پیش از C# 8.0، برای تعریف فیلدهای نال نپذیر و نال پذیر در موجودیت‌های EF Core، به صورت زیر عمل می‌شد:
    public class Person_BeforeCS8
    {
        [Required]
        public string FirstName { get; set; }  // NOT NULL

        public string MiddleName { get; set; } // NULL
    }
اگر رشته‌ای مزین به ویژگی Required باشد، یعنی به یک فیلد نال‌نپذیر، ترجمه و نگاشت خواهد شد و برعکس. اما پس از فعالسازی ویژگی نوع‌های ارجاعی نال نپذیر C# 8.0 در پروژه‌ی خود، کامپایلر اخطارهایی مانند «Non-nullable property 'FirstName' is uninitialized. Consider declaring the property as nullable» را به ازای تک تک خواص تعریف شده‌ی در کلاس موجودیت فوق، صادر می‌کند. برای رفع این مشکل می‌توان از bang operator که کمی بالاتر در مورد آن توضیح داده شده، استفاده کرد:
    public class Person_AfterCS8
    {
        public string FirstName { get; set; } = null!; // NOT NULL

        public string? MiddleName { get; set; } // NULL
    }
در اینجا نحوه‌ی تعریف دو فیلد نال‌نپذیر و نال پذیر را در موجودیت‌های EF Core 3.0 و C# 8.0 مشاهده می‌کنید. خاصیت اول دیگر نیازی به ویژگی Required ندارد؛ اما چون دیگر نال را نمی‌پذیرد، می‌توان مقدار دهی اولیه‌ی آن‌را توسط !null انجام داد؛ تا کامپایلر دیگر خطایی را در مورد عدم مقدار دهی اولیه‌ی آن صادر نکند (تنها کاربرد !null است). البته بهتر است !null را صرفا با EF Core و موجودیت‌های آن استفاده کنید و برای سایر کلاس‌ها، از دیگر روش‌های مطرح شده‌ی در این مطلب مانند تعریف یک سازنده، کمک بگیرید.
مزیت این روش نسبت به Person_BeforeCS8 این است که اینبار علاوه بر نال‌نپذیر تعریف شدن فیلد FirstName، نحوه‌ی استفاده‌ی از آن در برنامه (عدم انتساب نال به آن) نیز تحت کنترل کامپایلر قرار می‌گیرد که پیشتر با ویژگی Required چنین امری میسر نبود.
بنابراین در موجودیت‌های برنامه‌ی مبتنی بر C# 8.0، دیگر نیاز به استفاده‌ی از ویژگی Required نبوده و نال‌پذیری با عملگر ? مشخص می‌شود.


کار با وابستگی‌ها و ارتباط‌های نال‌پذیر

فرض کنید یک چنین کوئری را در EF Core 3.0 و C# 8.0 نوشته‌اید:
var parentPosts = db.Posts.Where(p => p.ParentPost.Id == postId).ToList();
در اینجا ParentPost می‌تواند نال باشد، اما در عمل EF Core به این موضوع اهمیتی نمی‌دهد و از آن صرفا جهت تهیه‌ی SQL نهایی استفاده می‌کند؛ اما کامپایلر C# 8.0، اخطار «Dereference of a possible null reference» را صادر می‌کند. برای رفع آن نیز می‌توان از bang operator استفاده کرد:
var parentPosts = db.Posts.Where(p => p.ParentPost!.Id == postId).ToList();
وجود عملگر ! در اینجا، به معنای اعلام صریح نال نبودن ParentPost، در شرایط کوئری فوق، به کامپایلر است.
مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامه‌های وب
الگوی Service locator را در قسمت دوم بررسی کردیم. همانطور که عنوان شد، بهتر است تا جائیکه امکان دارد از بکارگیری آن به علت ضدالگو بودن پرهیز کرد. در ادامه قسمت‌های مختلف یک برنامه‌ی ASP.NET Core را که می‌توان بدون نیاز به استفاده‌ی الگوی Service locator، تزریق وابستگی‌ها را در آن‌ها انجام داد، مرور می‌کنیم.


در کلاس آغازین برنامه

در اینجا در متد Configure آن تنها کافی است اینترفیس سرویس مدنظر خود را مانند IAmACustomService، به صورت یک پارامتر جدید اضافه کنید. کار وهله سازی آن توسط Service Provider برنامه به صورت خودکار صورت می‌گیرد:
public class Startup 
{ 
    public void ConfigureServices(IServiceCollection services) { } 
  
    public void Configure(IApplicationBuilder app, IAmACustomService customService) 
    { 
        // ....    
    }         
}

یک نکته‌ی مهم: اگر طول عمر IAmACustomService را Scoped تعریف کرده‌اید و این سرویس از نوع IDisposable نیز می‌باشد، این روش کارآیی نداشته و باید از نکته‌ی «روش صحیح Dispose اشیایی با طول عمر Scoped، در خارج از طول عمر یک درخواست ASP.NET Core» که در قسمت قبل معرفی شد استفاده کنید.


در میان افزارها

هم سازنده‌ی یک میان افزار و هم متد Invoke آن قابلیت تزریق وابستگی‌ها را دارند:
public class TestMiddleware 
{ 
    public TestMiddleware(RequestDelegate next, IAmACustomService service) 
    { 
        // ... 
    } 
 
    public async Task Invoke(HttpContext context, IAmACustomService service) 
    { 
        // ... 
    }     
}
از سازنده‌ی آن برای تزریق وابستگی سرویس‌هایی با طول عمر Singleton استفاده کنید. ServiceProvider به همراه ویژگی است به نام Scope Validation. در این حالت اگر طول عمر سرویسی Singleton باشد (مانند طول عمر یک میان‌افزار) و در سازنده‌ی آن یک سرویس با طول عمر Scoped تزریق شود، در زمان اجرا یک استثناء را صادر می‌کند؛ چون در این حالت رفتار این سرویس Scoped نیز Singleton می‌شود که احتمالا مدنظر شما نیست. در این حالت از پارامترهای اضافی متد Invoke می‌توان برای تزریق وابستگی‌هایی با طول عمر Transient و یا Scoped استفاده کرد.
البته می‌توان این Scope Validation را در فایل program.cs به نحو زیر غیرفعال کرد، ولی بهتر است اینکار را انجام ندهید و همان مقدار پیش‌فرض آن بسیار مناسب است:
public static IWebHostBuilder CreateDefaultBuilder(string[] args) 
{ 
            var builder = new WebHostBuilder() 
//...
                .UseDefaultServiceProvider((context, options) => 
                { 
                    options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); 
                }) 
//...

در کنترلرها

سازنده‌های کنترلرهای برنامه‌های ASP.NET Core قابلیت تزریق وابستگی‌ها را دارند:
public class HelloController : Controller 
{ 
    private readonly IAmACustomService _customService; 
 
    public HelloController(IAmACustomService customService) 
    { 
        _customService = customService; 
    } 
 
    public IActionResult Get() 
    { 
        // ... 
    } 
}
در اینجا حتی می‌توان با استفاده از ویژگی FromServices، یک سرویس را توسط پارامترهای یک اکشن متد نیز درخواست کرد:
[HttpGet("[action]")]
public IActionResult Index([FromServices] IAmACustomService service)
{  
   // ...
}
در این حالت بجای model binding، کار دریافت این سرویس درخواستی صورت می‌گیرد.


در مدل‌ها

ویژگی FromServices بر روی مدل‌ها نیز کار می‌کند.
public IActionResult Index(TestModel model)
{
  // ...
}
در اینجا نحوه‌ی تعریف TestModel را به همراه ویژگی FromServices مشاهده می‌کنید:
public class TestModel 
{        
    public string Name { get; set; } 
 
    [FromServices] 
    public IAmACustomService CustomService { get; set; } 
}
این حالت که property injection نیز نام دارد، نیاز به خاصیتی با یک public setter را دارد.


در Viewها

در Razor Views نیز می‌توان توسط inject directive@ کار تزریق وابستگی‌ها را انجام داد:
 @inject IAmACustomService CustomService
 

در ویژگی‌ها و فیلترها

در ASP.NET Core تزریق وابستگی‌های در سازنده‌های فیلترها نیز کار می‌کند:
public class ApiExceptionFilter : ExceptionFilterAttribute  
{  
    private ILogger<ApiExceptionFilter> _logger;  
    private IHostingEnvironment _environment;  
    private IConfiguration _configuration;  
  
    public ApiExceptionFilter(IHostingEnvironment environment, IConfiguration configuration, ILogger<ApiExceptionFilter> logger)  
    {  
        _environment = environment;  
         _configuration = configuration;  
         _logger = logger;  
    }
در این حالت چون سازنده‌ی این ویژگی، پارامتر دار شده‌است و این پارامترها نیز یک مقدار ثابت قابل کامپایل نیستند، برای معرفی یک چنین فیلتری باید از ServiceFilterها به صورت زیر استفاده کرد:
[Route("api/[controller]")]  
[ApiController]  
[ServiceFilter(typeof(ApiExceptionFilter))]  
public class ValuesController : ControllerBase  
{
نظرات مطالب
مروری بر کدهای کلاس SqlHelper
مایکروسافت پشتیبانی از پروایدر ADO.NET مرتبط به اوراکل را چندسالی است که رسما قطع کرده و خود اوراکل این پروایدر رو داره ارائه می‌ده. در مورد EF هم به همین صورت : (^) و (^) .
مطالب
آشنایی با سورس AndroidBreadCrumb

زمانی که سیستم عامل های GUI مثل ویندوز به بازار آمدند، یکی از قسمت‌های گرافیکی آن‌ها AddressBar   نام داشت که مسیر حرکت آن‌ها را در فایل سیستم نشان میداد و در سیستم عامل‌های متنی  CLI با دستور  cd یا pwd انجام می‌شد. بعدها در وب هم همین حرکت با نام BreadCrumb صورت گرفت که به عنوان مثال مسیر رسیدن به صفحه‌ی یک محصول یا یک مقاله را نشان می‌داد. در یک پروژه‌ی اندرویدی نیاز بود تا یک ساختار درختی را پیاده سازی کنم، ولی در برنامه‌های اندروید ایجاد یک درخت، کار هوشمندانه و مطلوبی نیست و روش کار به این صورت است که یک لیست از گروه‌های والد را نمایش داده و با انتخاب هر آیتم لیست به آیتم‌های فرزند تغییر میکند. حالا مسئله این بود که کاربر باید مسیر حرکت خودش را بشناسد. به همین علت مجبور شدم یک BreadCrumb را برای آن طراحی کنم که در زیر تصویر آن را مشاهده می‌کنید.


 از نکات جالب توجه در مورد این ماژول می‌توان گفت که قابلیت این را دارد تا تصمیمات خود را بر اساس اندازه‌های مختلف صفحه نمایش بگیرد. به عنوان مثال اگر آیتم‌های بالا بیشتر از سه عدد باشد و در صفحه جا نشود از یک مسیر جعلی استفاده می‌کند و همه‌ی آیتم‌ها با اندیس شماره 1 تا index-3 را درون یک آیتم با عنوان (...) قرار می‌دهد که من به آن می‌گویم مسیر جعلی. به عنوان نمونه مسیر تصویر بالا در صفحه جا شده است و نیازی به این کار دیده نشده است. ولی تصویر زیر از آن جا که مسیر، طول width صفحه نمایش رد کرده است، نیاز است تا چنین کاری انجام شود. موقعی‌که کاربر آیتم ... را کلیک کند، مسیر باز شده و به محل index-3 حرکت می‌کند. یعنی دو مرحله به عقب باز می‌گردد.


نگاهی به کارکرد ماژول 

قبل از توضیح در مورد سورس، اجازه دهید نحوه‌ی استفاده از آن را ببینیم.

این سورس شامل دو کلاس است که ساده‌ترین کلاس آن AndBreadCrumbItem می‌باشد که مشابه کلاس ListItem در بخش وب دات نت است و دو مقدار، یکی متن و دیگری Id را می‌گیرد:

سورس:

public class AndBreadCrumbItem {

    private int Id;
    private String diplayText;

    public AndBreadCrumbItem(int Id, String displayText)
    {
        this.Id=Id;
        this.diplayText=displayText;
    }
    public String getDiplayText() {
        return diplayText;
    }
    public void setDiplayText(String diplayText) {
        this.diplayText = diplayText;
    }
    public int getId() {
        return Id;
    }
    public void setId(int id) {
        Id = id;
    }
}

به عنوان مثال می‌خواهیم یک breadcrumb را با مشخصات زیر بسازیم:

AndBreadCrumbItem itemhome=new AndBreadCrumbItem(0,"Home");
AndBreadCrumbItem itemproducts=new AndBreadCrumbItem(12,"Products");
 AndBreadCrumbItem itemdigital=new AndBreadCrumbItem(15,"Digital");
AndBreadCrumbItem itemhdd=new AndBreadCrumbItem(56,"Hard Disk Drive");
حال از کلاس اصلی یعنی AndBreadCrumb استفاده می‌کنیم و آیتم‌ها را به آن اضافه می‌کنیم:
AndBreadCrumb breadCrumb=new AndBreadCrumb(this);

        breadCrumb.AddNewItem(itemhome);
        breadCrumb.AddNewItem(itemproducts);
        breadCrumb.AddNewItem(itemdigital);
        breadCrumb.AddNewItem(itemhdd);
به این نکته دقت داشته باشید که با هر شروع مجدد چرخه‌ی Activity، حتما شیء Context این کلاس را به روز نمایید تا در رسم المان‌ها به مشکل برنخورد. می‌توانید از طریق متد زیر context را مقداردهی نمایید:
breadCumb.setContext(this);
هر چند راه حل پیشنهادی این است که این کلاس را نگهداری ننماید و از یک لیست ایستا جهت نگهداری AndBreadCrumbItem‌ها استفاده کنید تا باهر بار  فراخوانی رویدادهای اولیه چون oncreate یا onstart و.. شی BreadCrumb را پر نمایید.

پس از افزودن آیتم ها، تنظیمات زیر را اعمال نمایید:

        LinearLayout layout=(LinearLayout)getActivity().findViewById(R.id.breadcumblayout);
        layout.setPadding(8, 8, 8, 8);
        breadCrumb.setLayout(layout);
        breadCrumb.SetTinyNextNodeImage(R.drawable.arrow);
        breadCrumb.setTextSize(25);
        breadCrumb.SetViewStyleId(R.drawable.list_item_style);
در سه خط اول، یک layout  از نوع Linear جهت رسم اشیاء به شیء breadcrumb معرفی می‌شود. سپس در صورت تمایل می‌توانید از یک شیء تصویر گرافیکی کوچک هم استفاده کنید که در تصاویر بالا می‌بینید از تصویر یک فلش جهت دار استفاده شده است تا بین هر المان ایجاد شده از آیتم‌ها قرار بگیرد. سپس در صورت تمایل اندازه‌ی قلم متون را مشخص می‌کنید و در آخر هم متد SetViewStyleId هم برای نسبت دادن یک استایل یا selector و ... استفاده می‌شود.
حال برای رسم آن متد UpdatePath را صدا می‌زنیم:
        breadCrumb.UpdatePath();

الان اگر برنامه اجرا شود باید breadcrumb از چپ به راست رسم گردد. برای استفاده‌های فارسی، راست به چپ می‌توانید از متد زیر استفاده کنید:
breadCrumb.setRTL(true);
در صورت هر گونه تغییری در تنظیمات، مجددا متد UpdatePath را فراخوانی کنید تا عملیات رسم، با تنظمیات جدید آغاز گردد.

در صورتیکه قصد دارید تنظیمات بیشتری چون رنگ متن، فونت متن و ... را روی هر المان اعمال کنید، از رویداد زیر استفاده کنید:

breadCrumb.setOnTextViewUpdate(new ITextViewUpdate() {
            @Override
            public TextView UpdateTextView(Context context, TextView tv) {
                tv.setTextColor(...);
                tv.setTypeface(...);
                return tv;
            }
        });
با هر بار ایجاد المان که از نوع TextView است، این رویداد فراخوانی شده و تنظیمات شما را روی آن اجرا می‌کند.
همچنین در صورتیکه می‌خواهید بدانید کاربر بر روی چه عنصری کلیک کرده است، از رویداد زیر استفاده کنید:
breadCumb.setOnClickListener(new IClickListener() {
            @Override
            public void onClick(int position, int Id) {
                  //...
            }
        });
کد بالا دو آرگومان را ارسال میکند که اولی position یا اندیس مکانی عنصر کلیک شده را بر می‌گرداند و دومی id هست که با استفاده ازکلاس AndBreadCrumbItem به آن پاس کرده‌اید. هنگام کلیک کاربر روی عنصر مورد نظر، برگشت به عقب به طور خودکار صورت گرفته و عناصر بعد از آن موقعیت، به طور خودکار حذف خواهند شد.

آخرین متد موجود که کمترین استفاده را دارد، متد SetNoResize است. در صورتیکه این متد با True مقداردهی گردد، عملیات تنظیم بر اساس صفحه‌ی نمایش لغو می‌شود. این متد برای زمانی مناسب است که به عنوان مثال شما از یک HorozinalScrollView استفاده کرده باشید. در این حالت layout شما هیچ گاه به پایان نمی‌رسد و بهتر هست عملیات اضافه را لغو کنید.

نگاهی به سورس

  کلاس زیر شامل بخش‌های زیر است:
فیلدهای خصوصی
 //=-=--=-=-=-=-=-=-=-=-=-=-=- Private Properties -=-=-=-=-=-=-=--=-=-=
    private List<AndBreadCrumbItem> items=null;
    private List<TextView> textViews;
    private int tinyNextNodeImage;
    private int viewStyleId;
    private Context context;
    private boolean RTL;
    private float textSize=20;
    private boolean noResize=false;

    LinearLayout layout;
    IClickListener clickListener;
    ITextViewUpdate textViewUpdate;
    LinearLayout.LayoutParams params ;

با نگاهی به نام آن‌ها میتوان حدس زد که برای چه کاری استفاده می‌شوند. به عنوان نمونه از اصلی‌ترین‌ها، متغیر items جهت نگهداری آیتم‌های پاس شده استفاده می‌شود و textviews هم برای نگهداری هر breadcrumb یا همان المان TextView که روی صفحه رسم می‌شود.
اینترفیس‌ها هم با حرف I شروع و برای تعریف رویدادها ایجاد شده‌اند. در ادامه از تعدادی متد get و Set برای مقدار دهی بعضی از فیلدهای خصوصی بالا استفاده شده است:
    //=-=---=-=-=-=-- Constructor =--=-=-=-=-=--=-=-

    public AndBreadCrumb(Context context)
    {
        this.context=context;
        params = new LinearLayout.LayoutParams
                (LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
    }

    //=-=-=--=--=-=-=-=-=-=-=-=-  Public Properties --=-=-=-=-=-=--=-=-=-=-=-=-

    //each category would be added to create path
    public void AddNewItem(AndBreadCrumbItem item)
    {
        if(items==null)
            items=new ArrayList<>();
        items.add(item);
    }

    // if you want a pointer or next node between categories or textviews
    public void SetTinyNextNodeImage(int resId) {this.tinyNextNodeImage=resId;}

    public void SetViewStyleId(int resId) {this.viewStyleId=resId;}

    public void setTextSize(float textSize) {this.textSize = textSize;}

    public boolean isRTL() {
        return RTL;
    }

    public void setRTL(boolean RTL) {
        this.RTL = RTL;
    }

    public void setLayout(LinearLayout layout) {

        this.layout = layout;
    }

    public void setContext(Context context) {
        this.context = context;
    }

    public boolean isNoResize() {
        return noResize;
    }

    public void setNoResize(boolean noResize) {
        this.noResize = noResize;
    }

بعد از آن به متدهای خصوصی می‌رسیم که متد زیر، متد اصلی ما برای ساخت breadcrumb است:
 //primary method for render objects on layout
    private void DrawPath() {


        //stop here if essentail elements aren't present
        if (items == null) return ;
        if (layout == null) return;
        if (items.size() == 0) return;


//we need to get size of layout,so we use the post method to run this thread when ui is ready
        layout.post(new Runnable() {
            @Override
            public void run() {


                //textviews created here one by one
                int position = 0;
                textViews = new ArrayList<>();
                for (AndBreadCrumbItem item : items) {
                    TextView tv = MakeTextView(position, item.getId());
                    tv.setText(item.getDiplayText());
                    textViews.add(tv);
                    position++;
                }


                //add textviews on layout
                AddTextViewsOnLayout();

                //we dont manage resizing anymore
                if(isNoResize()) return;

                //run this code after textviews Added to get widths of them
                TextView last_tv=textViews.get(textViews.size()-1);
                last_tv.post(new Runnable() {
                    @Override
                    public void run() {
                        //define width of each textview depend on screen width
                        BatchSizeOperation();
                    }
                });

            }
        });


    }
متد DrawPath برای ترسیم breadcumb است و می‌توان گفت اصلی‌ترین متد این کلاس است. در سه خط اول، عناصر الزامی را که باید مقداردهی شده باشند، بررسی می‌کند. این موارد وجود آیتم‌ها و layout است. اگر هیچ یک از اینها مقدار دهی نشده باشند، عملیات رسم خاتمه می‌یابد. بعد از آن یک پروسه‌ی UI جدید را در متد post شیء Layout معرفی می‌کنیم. این متد زمانی این پروسه را صدا می‌زند که layout در UI برنامه جا گرفته باشد. دلیل اینکار این است که تا زمانی که ویوها در UI تنظیم نشوند، نمی‌توانند اطلاعاتی چون پهنا و ارتفاع را برگردانند و همیشه مقدار 0 را باز می‌گردانند. پس ما بامتد post اعلام می‌کنیم زمانی این پروسه را اجرا کن که وضعیت UI خود را مشخص کرده‌ای.
به عنوان نمونه کد زیر را ببینید:
TextView tv=new TextView(this);
tv.getWidth(); //return 0
layout.add(tv);
tv.getWidth(); //return 0
در این حالت کنترل در هر صورتی عدد ۰ را به شما باز می‌گرداند و نمی‌توانید اندازه‌ی آن را بگیرید مگر اینکه درخواست یک callback بعد از رسم را داشته باشید که این کار از طریق متد post انجام می‌گیرد:
TextView tv=new TextView(this);
tv.post(new Runnable() {
                    @Override
                    public void run() {
                        tv.getWidth(); //return x
                    }
                });
در اینجا مقدار واقعی x بازگردانده می‌شود.

باز می‌گردیم به متد DrawPath و داخل متد post
 در اولین خط این پروسه به ازای هر آیتم، یک TextView توسط متد MakeTextView ساخته می‌شود که شامل کد زیر است:
  private TextView MakeTextView(final int position, final int Id)
    {
        //settings for cumbs
        TextView tv=new TextView(this.context);
        tv.setEllipsize(TextUtils.TruncateAt.END);
        tv.setSingleLine(true);
        tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
        tv.setBackgroundResource(viewStyleId);

        /*call custom event - this event will be fired when user click on one of
         textviews and returns position of textview and value that user sat as id
         */
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                SetPosition(position);
                clickListener.onClick(position, Id);
            }
        });

        //if user wants to update each textviews
        if(textViewUpdate!=null)
            tv=textViewUpdate.UpdateTextView(context,tv);

        if(isRTL())
            tv.setRotationY(180);

        return tv;
    }

در خطوط اولیه، یک Textview ساخته و متد Ellipsize را با Truncate.END مقداردهی می‌نماید. این مقدار دهی باعث می‌شود اگر متن، در Textview جا نشد، ادامه‌ی آن با ... مشخص شود. در خط بعدی Textview را تک خطه معرفی می‌کنیم. در خط بعدی اندازه‌ی قلم را بر اساس آنچه کاربر مشخص کرده است، تغییر می‌دهیم و بعد هم استایل را برای آن مقداردهی می‌کنیم. بعد از آن رویداد کلیک را برای آن مشخص می‌کنیم تا اگر کاربر بر روی آن کلیک کرد، رویداد اختصاصی خودمان را فراخوانی کنیم.
در خط بعدی اگر rtl با true مقدار دهی شده باشد، textview را حول محور Y چرخش می‌دهد تا برای زبان‌های راست به چپ چون فارسی آماده گردد و در نهایت Textview ساخته شده و به سمت متد DrawPath باز می‌گرداند.

بعد از ساخته شدن TextViewها، وقت آن است که به Layout اضافه شوند که وظیفه‌ی اینکار بر عهده‌ی متد AddTextViewOnLayout است:
 //this method calling by everywhere to needs add textviews on the layout like master method :drawpath
    private void AddTextViewsOnLayout()
    {
        //prepare layout
        //remove everything on layout for recreate it
        layout.removeAllViews();
        layout.setOrientation(LinearLayout.HORIZONTAL);
        layout.setVerticalGravity(Gravity.CENTER_VERTICAL);
        if(isRTL())
            layout.setRotationY(180);



        //add textviews one by one

        int position=0;
        for (TextView tv:textViews)
        {
            layout.addView(tv,params);

            //add next node image between textviews if user defined a next node image
            if(tinyNextNodeImage>0)
                if(position<(textViews.size()-1)) {
                    layout.addView(GetNodeImage(), params);
                    position++;
                }
        }

    }

در چند خط اول، Layout آماده سازی می‌شود. این آماده سازی شامل پاکسازی اولیه Layout یا خالی کردن ویوهای درون آن است که می‌تواند از رندر قبلی باشد. افقی بودن جهت چینش Layout، در مرکز نگاه داشتن ویوها و نهایتا چرخش حول محور Y در صورت true بودن خاصیت RTL است. در خطوط بعدی یک حلقه وجود دارد که Textview‌های ایجاد شده را یک به یک در Layout می‌چیند و اگر کاربر تصویر گرافیکی را هم به (همان فلش‌های اشاره‌گر) متغیر tinyNextNodeImage نسبت داده باشد، آن‌ها را هم بین TextView‌ها می‌چیند و بعد از پایان یافتن کار، مجددا به متد DrawPath باز می‌گردد.
تا به اینجا کار چیدمان به ترتیب انجام شده است ولی از آنجا که اندازه‌ی Layout در هر گوشی و  در دو حالت حالت افقی یا عمودی نگه داشتن گوشی متفاوت است، نمی‌توان به این چینش اعتماد کرد که به چه نحوی عناصر نمایش داده خواهند شد و این مشکل توسط متد BatchSizeOperation (تغییر اندازه دسته جمعی) حل می‌گردد. در اینجا هم باز متد post به آخرین textview اضافه شده است. به این علت که موقعی‌که همه‌ی textview‌ها در ui جا خوش کردند، بتوانیم به خاصیت‌های ui آن‌ها دستیابی داشته باشیم. حالا بعد از ترسیم باید اندازه آن‌ها را اصلاح کنیم. قدم به قدم متد BatchSizeOperation را بررسی می‌کنیم:
//set textview width depend on screen width
private void BatchSizeOperation()
{
//get width of next node between cumbs
Bitmap tinyBmap = BitmapFactory.decodeResource(context.getResources(), tinyNextNodeImage);
int tinysize=tinyBmap.getWidth();
//get sum of nodes
tinysize*=(textViews.size()-1);
...
}
ابتدا لازم است ‍‍‍‍‍طول مسیری که همه ویوها یا المان‌های ما را دارند، به دست آوریم. اول از تصویر کوچک شروع می‌کنیم و پهنای آن را می‌گیریم. سپس عدد به دست آمده را در تعداد آن ضرب می‌کنیم تا جمع پهناها را داشته باشیم. سپس نوبت به TextView‌ها می‌رسد.

  //get width size of screen(layout is screen here)
        int screenWidth=GetLayoutWidthSize();

        //get sum of arrows and cumbs width
        int sumtvs=tinysize;
        for (TextView tv : textViews) {

            int width=tv.getWidth();
            sumtvs += width;
        }
در ادامه‌ی این متد، متد GetLayoutWidthSize را صدا می‌زنیم که وظیفه‌ی آن برگرداندن پهنای layout است و کد آن به شرح زیر است:
    private int GetLayoutWidthSize()
    {
        int width=layout.getWidth();
        int padding=layout.getPaddingLeft()+layout.getPaddingRight();
        width-=padding;
        return width;
    }
در این متد پهنا به احتساب padding‌های چپ و راست به دست می‌آید و مقدار آن را به عنوان اندازه‌ی صفحه نمایش، تحویل متد والد می‌دهد. در ادامه هم پهنای هر Textview محاسبه شده و جمع کل آن‌ها را با اندازه‌ی صفحه مقایسه می‌کند. اگر کوچکتر بود، کار این متد در اینجا تمام می‌شود و نیازی به تغییر اندازه نیست. ولی اگر نبود کد ادامه می‌یابد:
    private void  BatchSizeOperation()
    {
        ....

    //if sum of cumbs is less than screen size the state is good so return same old textviews
        if(sumtvs<screenWidth)
            return ;


        if(textViews.size()>3)
        {
            //make fake path
            MakeFakePath();

            //clear layout and add textviews again
            AddTextViewsOnLayout();
        }

        //get free space without next nodes -> and spilt rest of space to textviews count to get space for each textview
        int freespace =screenWidth-tinysize;
        int each_width=freespace/textViews.size();

        //some elements have less than each_width,so we should leave size them and calculate more space again
        int view_count=0;
        for (TextView tv:textViews)
        {
            if (tv.getWidth()<=each_width)
                freespace=freespace-tv.getWidth();
            else
                view_count++;
        }
        if (view_count==0) return;

        each_width=freespace/view_count;
        for (TextView tv:textViews)
        {
            if (tv.getWidth()>each_width)
                tv.setWidth(each_width);
        }


    }

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

نحوه‌ی کارکرد متد MakeFakePath بدین صورت است که 4 عدد TextView را ایجاد کرده که المان‌های با اندیس 0 و 2 و 3 به صورت نرمال و عادی ایجاد شده و همان کارکرد سابق را دارند. ولی المان شماره دو با اندیس 1 با متن ... نماینده‌ی آیتم‌های میانی است و رویدادکلیک  آن به شکل زیر تحریف یافته است:

 //if elements are so much(mor than 3),we make a fake path to decrease elements
    private void MakeFakePath()
    {
        //we make 4 new elements that index 1 is fake element and has a rest of real path in its heart
        //when user click on it,path would be opened
        textViews=new ArrayList<>(4);
        TextView[] tvs=new TextView[4];
        int[] positions= {0,items.size()-3,items.size()-2,items.size()-1};

        for (int i=0;i<4;i++)
        {
            //request for new textviews
            tvs[i]=MakeTextView(positions[i],items.get(positions[i]).getId());

            if(i!=1)
                tvs[i].setText(items.get(positions[i]).getDiplayText());
            else {
                tvs[i].setText("...");
                //override click event and change it to part of code to open real path by call setposition method and redraw path
                tvs[i].setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        int pos = items.size() - 3;
                        int id = items.get(pos).getId();
                        SetPosition(items.size() - 3);
                        clickListener.onClick(pos, id);
                    }
                });
            }
            textViews.add(tvs[i]);
        }
    }
این رویداد با استفاده از setPosition به آیتم index-3 بازگشته و مجددا المان‌ها رسم می‌گردند و سپس رویداد کلیک این آیتم را هم اجرا می‌کند و المان‌های با اندیس 2 و 3 را به ترتیب به رویدادهای index-1 و index-2 متصل می‌کنیم.
مطالب
پیاده سازی RabbitMQ
RabbitMq شبیه به یک صف FIFO عمل میکند؛ یعنی داده‌ها به ترتیب وارد queue میشوند و به ترتیب نیز به Consumer‌ها ارسال میشوند. برای شروع، یک سولوشن جدید را به نام RabbitMqExample ایجاد میکنیم و پروژه‌های زیر را به آن اضافه میکنیم.
  • یک پروژه از نوع Asp.Net Core Web Application ایجاد میکنیم به نام RabbiMqExample.Producer که همان ارسال کننده (Producer) میباشد.
  • یک پروژه از نوع Asp.Net Core Web Application به نام RabbitMqExample.Consumer برای دریافت کننده (Consumer).
  • یک پروژه از نوع Class library .Net Core به نام RabbitMqExample.Common که شامل سرویس‌ها و مدل‌های مشترک بین Producer و Consumer میباشد.
ابتدا در لایه Common یک کلاس برای دریافت اطلاعات RabbitMq از appsettings.json ایجاد میکنیم.
public class RabbitMqConfiguration
{
    public string HostName { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}
سپس یک سرویس را برای برقراری ارتباط با RabbitMq ایجاد میکنیم
public interface IRabbitMqService
{
    IConnection CreateChannel();
}

public class RabbitMqService : IRabbitMqService
{
    private readonly RabbitMqConfiguration _configuration;
    public RabbitMqService(IOptions<RabbitMqConfiguration> options)
    {
        _configuration = options.Value;
    }
    public IConnection CreateChannel()
    {
        ConnectionFactory connection = new ConnectionFactory()
        {
            UserName = _configuration.Username,
            Password = _configuration.Password,
            HostName = _configuration.HostName
        };
        connection.DispatchConsumersAsync = true;
        var channel = connection.CreateConnection();
        return channel;
    }
}
در متد CreateChannel، اطلاعات موردنیاز برای ارتباط با RabbitMq را مانند هاست، نام کاربری و کلمه عبور، وارد میکنیم که از appsettings.json خوانده شده‌اند. مقدار پیش‌فرض نام کاربری و کلمه عبور، guest میباشد.
 اگر بخواهید Consumer شما داده‌های queue‌ها را به صورت async دریافت کند، باید مقدار پراپرتی DispatchConsumersAsync مربوط به ConnectionFactory را برابر true کنید. مقدار پیشفرض آن false است.  
در ادامه یک کلاس را برای رجیستر کردن سرویس‌ها ایجاد میکنیم؛ در لایه Common.
public static class StartupExtension
{
    public static void AddCommonService(this IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<RabbitMqConfiguration>(a => configuration.GetSection(nameof(RabbitMqConfiguration)).Bind(a));
        services.AddSingleton<IRabbitMqService, RabbitMqService>();
    }
}
پکیج‌های مورد نیاز این لایه :
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
    <PackageReference Include="RabbitMQ.Client" Version="6.2.1" />
  </ItemGroup>
تا به اینجا موارد مربوط به لایه Common تمام شده؛ در ادامه باید یک Consumer و یک Producer را ایجاد کنیم.
در لایه Consumer برای دریافت داده‌ها از RabbitMq؛ یک سرویس را به نام ConsumerService ایجاد میکنیم:
public interface IConsumerService
{
    Task ReadMessgaes();
}

public class ConsumerService : IConsumerService, IDisposable
{
    private readonly IModel _model;
    private readonly IConnection _connection;
    public ConsumerService(IRabbitMqService rabbitMqService)
    {
        _connection = rabbitMqService.CreateChannel();
        _model = _connection.CreateModel();
        _model.QueueDeclare(_queueName, durable: true, exclusive: false, autoDelete: false);
        _model.ExchangeDeclare("UserExchange", ExchangeType.Fanout, durable: true, autoDelete: false);
        _model.QueueBind(_queueName, "UserExchange", string.Empty);
    }
    const string _queueName = "User";
    public async Task ReadMessgaes()
    {
        var consumer = new AsyncEventingBasicConsumer(_model);
        consumer.Received += async (ch, ea) =>
        {
            var body = ea.Body.ToArray();
            var text = System.Text.Encoding.UTF8.GetString(body);
            Console.WriteLine(text);
            await Task.CompletedTask;
            _model.BasicAck(ea.DeliveryTag, false);
        };
        _model.BasicConsume(_queueName, false, consumer);
        await Task.CompletedTask;
    }

    public void Dispose()
    {
        if (_model.IsOpen)
            _model.Close();
        if (_connection.IsOpen)
            _connection.Close();
    }
}
ابتدا connection را ایجاد کرده‌ایم؛ توسط متد CreateChannel که آنرا در سرویس قبلی پیاده سازی کردیم.
بعد از ایجاد IModel، باید queue مربوطه را معرفی کنیم که با استفاده از متد QueueDeclare این کار را انجام داده‌ایم. 
پارامترهای متد QueueDeclare:
  • پارامتر اول، اسم queue میباشد 
  • پارامتر durable مشخص میکند که داده‌ها به صورت مانا باشند یا نه. اگر برابر true باشد، دیتاهای مربوط به queue‌ها، در دیسک ذخیره میشوند؛ اما اگر برابر false باشد، بر روی حافظه ذخیره میشوند. در محیط‌هایی که مانایی اطلاعات مهم میباشد، باید مقدار این پارامتر را true کنید.
  • پارامتر سوم: اطلاعات بیشتر
  • پارامتر autoDelete اگر برابر true باشد، زمانی که تمامی Consumer‌ها ارتباطشان با RabbitMq قطع شود، queue هم پاک میشود. اما اگر برابر true باشد، queue باقی میماند؛ حتی اگر هیچ Consumer ای به آن وصل نباشد.
در ادامه باید Exchange مربوط به queue را مشخص کنیم. متد ExchangeDeclare یک Exchange را ایجاد میکند. پارامتر‌های متد ExchangeDeclare:
  • نام Exchange
  • نوع Exchange که میتواند Headers , Topic ,  Fanout یا Direct باشد. اگر برابر Fanout باشد و اگر داده‌ای وارد Exchange شود، آن‌را به تمامی queue هایی که به آن بایند شده‌است، ارسال میکند. اما اگر نوع آن Direct باشد، داده را به یک queue مشخص ارسال میکند؛ با استفاده از پارامتر routeKey.
  • پارامتر‌های بعدی، durable و autoDelete هستند که همانند پارامترهای QueueDeclare عمل میکنند.
سپس در ادامه با استفاده از متد QueueBind میتوانیم queue ایجاد شده را به exchange ایجاد شده، بایند کنیم. پارامتر اول، اسم queue و پارامتر دوم، اسم exchange میباشد و پارامتر سوم، routeKey میباشد و چون نوع Exchange ایجاد شده از نوع Fanout است، آنرا خالی میگذاریم. 

چون هنگام تعریف queue مقدار پارامتر DispatchConsumersAsync مربوط به ConnectionFactory را برابر true کردیم، در اینجا نیز باید بجای EventingBasicConsumer، از AsyncEventingBasicConsumer استفاده کنیم. اگر مقدار DispatchConsumersAsync برابر false باشد، باید از EventingBasicConsumer برای ایجاد Consumer استفاده کنید. 

سپس باید EventHandler مربوط به دریافت داده‌ها از queue را پیاده سازی کنیم. event مربوط به Received، زمانی اجرا میشود که داده‌ای به queue ارسال شود. زمانیکه داده‌ای ارسال میشود، وارد event مربوطه میشود و ابتدا آنرا به صورت byte دریافت میکنیم. سپس رشته‌ی ارسالی آن‌را توسط متد GetString، بدست می‌آوریم و داده‌ی ارسال شده را در صفحه‌ی کنسول نمایش میدهیم.

 در ادامه به RabbitMq اطلاع میدهیم که داده‌ای که ارسال شده برای queue، توسط Consumer دریافت شده؛ با استفاده از متد BasicAck. این کار یک delivery  به RabbitMq ارسال میکند تا دیتای ارسال شده را را پاک کند. اگر این متد را فراخوانی نکنیم، هربار که برنامه اجرا میشود، تمامی دیتاهای قبلی را مجددا دریافت میکنیم و تا زمانیکه delivery را به RabbitMq نفرستیم، داده‌ها را پاک نمیکند.

نکته آخر در Consumer، متد BasicConsume است که عملا Consumer ایجاد شده را به RabbitMq معرفی میکند. برای دریافت داده‌ها و ثبت Consumer، نیازمند آن هستیم تا یکبار متد ReadMessage فراخوانی شود. برای همین یک HostedService ایجاد میکنیم تا یکبار این متد را فراخوانی کند:
public class ConsumerHostedService : BackgroundService
{
    private readonly IConsumerService _consumerService;

    public ConsumerHostedService(IConsumerService consumerService)
    {
        _consumerService = consumerService;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _consumerService.ReadMessgaes();
    }
}
در نهایت سرویس‌های ایجاد شده را رجیستر میکنیم؛ در Startup لایه Consumer
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; set; }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCommonService(Configuration);
        services.AddSingleton<IConsumerService, ConsumerService>();
        services.AddHostedService<ConsumerHostedService>();
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
    }
}
تا به اینجا کارهای مربوط به Consumer تمام شده و باید قسمت Producer آنرا پیاده سازی کنیم.
در لایه Producer یک کنترلر به نام RabbitController را ایجاد میکنیم که شامل یک متد میباشد که داده‌ها را به Queue ارسال میکند:
[Route("api/[controller]/[action]")]
[ApiController]
public class RabbitController : ControllerBase
{
    private readonly IRabbitMqService _rabbitMqService;

    public HomeController(IRabbitMqService rabbitMqService)
    {
        _rabbitMqService = rabbitMqService;
    }
    [HttpPost]
    public IActionResult SendMessage()
    {
        using var connection = _rabbitMqService.CreateChannel();
        using var model = connection.CreateModel();
        var body = Encoding.UTF8.GetBytes("Hi");
        model.BasicPublish("UserExchange",
                             string.Empty,
                             basicProperties: null,
                             body: body);

        return Ok();
    }
}
در متد SendMessage، ابتدا ارتباط خود را با RabbitMq برقرار میکنیم و سپس دیتای "Hi" را به صورت byte، به RabbitMq ارسال میکنیم؛ توسط متد BasicPublish.
پارامتر اول، اسم Exchange است و پارامتر دوم، routeKey و body هم دیتای ارسالی میباشد. در نهایت سرویس‌های مربوط به لایه Producer را رجیستر میکنیم؛ در Startup لایه Consumer:
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; set; }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddCommonService(Configuration);
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
        });
    }
}
اکنون اگر هر دو پروژه را اجرا کنید و متد SendMessage مربوط به کنترلر Rabbit را فراخوانی کنید، بعد از آنکه پیام شما ارسال شد، در صفحه کنسول مربوط به Consumer، رشته ارسال شده را مشاهده میکنید.
فایل appsetting.json مربوط به پروژه‌های Consumer و Producer:
{
  "RabbitMqConfiguration": {
    "HostName": "localhost",
    "Username": "guest",
    "Password": "guest"
  }
}
فایل docker-compose.yml برای اجرای RabbitMq بر روی داکر:
version: "3.2"
services:
  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: 'rabbitmq'
    ports:
        - 5672:5672
        - 15672:15672
کدهای این مقاله را میتوانید از گیت‌هاب دانلود کنید.
مطالب
استفاده از Kendo UI templates
در مطلب «صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid» در انتهای بحث، ستون IsAvailable به صورت زیر تعریف شد:
columns: [
               {
                   field: "IsAvailable", title: "موجود است",
                   template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>'
                }
]
Templates، جزو یکی از پایه‌های Kendo UI Framework هستند و توسط آن‌ها می‌توان قطعات با استفاده‌ی مجدد HTML ایی را طراحی کرد که قابلیت یکی شدن با اطلاعات جاوا اسکریپتی را دارند.
همانطور که در این مثال نیز مشاهده می‌کنید، قالب‌های Kendo UI از Hash (#) syntax استفاده می‌کنند. در اینجا قسمت‌هایی از قالب که با علامت # محصور می‌شوند، در حین اجرا، با اطلاعات فراهم شده جایگزین خواهند شد.
برای رندر مقادیر ساده می‌توان از # =# استفاده کرد. از # :# برای رندر اطلاعات HTML-encoded کمک گرفته می‌شود و #  # برای رندر کدهای جاوا اسکریپتی کاربرد دارد. از حالت HTML-encoded برای نمایش امن اطلاعات دریافتی از کاربران و جلوگیری از حملات XSS استفاده می‌شود.
اگر در این بین نیاز است # به صورت معمولی رندر شود، در حالت کدهای جاوا اسکریپتی به صورت #\\ و در HTML ساده به صورت #\ باید مشخص گردد.


مثالی از نحوه‌ی تعریف یک قالب Kendo UI

    <!--دریافت اطلاعات از منبع محلی-->
    <script id="javascriptTemplate" type="text/x-kendo-template">
        <ul>
            # for (var i = 0; i < data.length; i++) { #
            <li>#= data[i] #</li>
            # } #
        </ul>
    </script>

    <div id="container1"></div>
    <script type="text/javascript">
        $(function () {
            var data = ['User 1', 'User 2', 'User 3'];
            var template = kendo.template($("#javascriptTemplate").html());
            var result = template(data); //Execute the template
            $("#container1").html(result); //Append the result
        });
    </script>
این قالب ابتدا در تگ script محصور می‌شود و سپس نوع آن مساوی text/x-kendo-template قرار می‌گیرد. در ادامه توسط یک حلقه‌ی جاوا اسکریپتی، عناصر آرایه‌ی فرضی data خوانده شده و با کمک Hash syntax در محل‌های مشخص شده قرار می‌گیرند.
در ادامه باید این قالب را رندر کرد. برای این منظور یک div با id مساوی container1 را جهت تعیین محل رندر نهایی اطلاعات مشخص می‌کنیم. سپس متد kendo.template بر اساس id قالب اسکریپتی تعریف شده، یک شیء قالب را تهیه کرده و سپس با ارسال آرایه‌ای به آن، سبب اجرای آن می‌شود. خروجی نهایی، یک قطعه کد HTML است که در محل container1 درج خواهد شد.
همانطور که ملاحظه می‌کنید، متد kendo.template، نهایتا یک رشته را دریافت می‌کند. بنابراین همینجا و به صورت inline نیز می‌توان یک قالب را تعریف کرد.


کار با منابع داده راه دور

فرض کنید مدل برنامه به صورت ذیل تعریف شده‌است:
namespace KendoUI04.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}
و لیستی از آن توسط یک ASP.NET Web API کنترلر، به سمت کاربر ارسال می‌شود:
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using KendoUI04.Models;

namespace KendoUI04.Controllers
{
    public class ProductsController : ApiController
    {
        public IEnumerable<Product> Get()
        {
            return ProductDataSource.LatestProducts.Take(10);
        }
    }
}
در سمت کاربر و در View برنامه خواهیم داشت:
    <!--دریافت اطلاعات از سرور-->
    <div>
        <div id="container2"><ul></ul></div>
    </div>

    <script id="template1" type="text/x-kendo-template">
        <li> #=Id# - #:Name# - #=kendo.toString(Price, "c")#</li>
    </script>

    <script type="text/javascript">
        $(function () {
            var producatsTemplate1 = kendo.template($("#template1").html());

            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                change: function () {
                    $("#container2 > ul").html(kendo.render(producatsTemplate1, this.view()));
                }
            });
            productsDataSource.read();
        });
    </script>
ابتدا یک div با id مساوی container2 جهت تعیین محل نهایی رندر قالب template1 در صفحه تعریف می‌شود.
هرچند خروجی دریافتی از سرور نهایتا یک آرایه از اشیاء Product است، اما در template1 اثری از حلقه‌ی جاوا اسکریپتی مشاهده نمی‌شود. در اینجا چون از متد kendo.render استفاده می‌شود، نیازی به ذکر حلقه نیست و به صورت خودکار، به تعداد عناصر آرایه دریافتی از سرور، قطعه HTML قالب را تکرار می‌کند.
در ادامه برای کار با سرور از یک Kendo UI DataSource استفاده شده‌است. قسمت transport/read آن، کار تعریف محل دریافت اطلاعات را از سرور مشخص می‌کند. رویدادگران change آن اطلاعات نهایی دریافتی را توسط متد view در اختیار متد kendo.render قرار می‌دهد. در نهایت، قطعه‌ی HTML رندر شده‌ی نهایی حاصل از اجرای قالب، در بین تگ‌های ul مربوط به container2 درج خواهد شد.
رویدادگران change زمانیکه data source، از اطلاعات راه دور و یا یک آرایه‌ی جاوا اسکریپتی پر می‌شود، فراخوانی خواهد شد. همچنین مباحث مرتب سازی اطلاعات، صفحه بندی و تغییر صفحه، افزودن، ویرایش و یا حذف اطلاعات نیز سبب فراخوانی آن می‌گردند. متد view ایی که در این مثال فراخوانی شد، صرفا در روال رویدادگردان change دارای اعتبار است و آخرین تغییرات اطلاعات و آیتم‌های موجود در data source را باز می‌گرداند.


یک نکته‌ی تکمیلی: فعال سازی intellisense کدهای جاوا اسکریپتی Kendo UI

اگر به پوشه‌ی اصلی مجموعه‌ی Kendo UI مراجعه کنید، یکی از آن‌ها vsdoc نام دارد که داخل آن فایل‌های min.intellisense.js و vsdoc.js مشهود هستند.
اگر از ویژوال استودیوهای قبل از 2012 استفاده می‌کنید، نیاز است فایل‌های vsdoc.js متناظری را به پروژه اضافه نمائید؛ دقیقا در کنار فایل‌های اصلی js موجود. اگر از ویژوال استودیوی 2012 و یا بالاتر استفاده می‌کنید باید از فایل‌های intellisense.js متناظر استفاده کنید. برای مثال اگر از kendo.all.min.js کمک می‌گیرید، فایل متناظر با آن kendo.all.min.intellisense.js خواهد بود.
بعد از اینکار نیاز است فایلی به نام references.js_ را به پوشه‌ی اسکریپت‌های خود با این محتوا اضافه کنید (برای VS 2012 به بعد):
/// <reference path="jquery.min.js" />
/// <reference path="kendo.all.min.js" />
نکته‌ی مهم اینجا است که این فایل به صورت پیش فرض از مسیر Scripts/_references.js/~ خوانده می‌شود. برای اضافه کردن مسیر دیگری مانند js/_references.js/~ باید آن‌را به تنظیمات ذیل اضافه کنید:
 Tools menu –> Options -> Text Editor –> JavaScript –> Intellisense –> References
گزینه‌ی Reference Group را به (Implicit (Web تغییر داده و سپس مسیر جدیدی را اضافه نمائید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
KendoUI04.zip
نظرات مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی
تعدادی پرسش و پاسخ تکمیلی مباحث مسیریابی

چگونه می‌توان DateTime را به عنوان پارامتر مسیریابی ارسال کرد؟
Blazor از پارامترهایی از نوع DateTime، در حین مسیریابی پشتیبانی می‌کند؛ با این شرط که قید datetime در حین مسیریابی صریحا ذکر شود:
@page "/route/{parameter:datetime}"
یک مثال: کامپوننت index.razor که سبب هدایت به سمت کامپوننت دیگری به همراه ارسال پارامتری از نوع DateTime می‌شود:
@page "/"
@inject NavigationManager NavManager
<button @onclick="CurrentTime">Current Time</button>
@code {
    public void CurrentTime()
    {
        NavManager.NavigateTo("/time/" + DateTime.Now);
    }
}
و کامپوننت Time.razor که قادر است این DateTime را دریافت کرده و به یک پارامتر، Bind کند:
@page "/time/{param:datetime}"
<h3>Time</h3>
<p>@Param</p>
@code {
    [Parameter]
    public DateTime Param { get; set; }
}


چگونه می‌توان به عنوان صفحه‌ی جاری دسترسی یافت؟
برای اینکار نیاز است از JavaScript interop استفاده کرد. ابتدا برای مثال تابع عمومی getTitle را که بر اساس DOM API مرورگر کار می‌کند، تهیه می‌کنیم:
window.getTitle = () => {
   return document.title;
};
سپس می‌توان از آن در یک کامپوننت Blazor به صورت زیر استفاده کرد:
@page "/"
@inject IJSRuntime jsRuntime
<h2>Page Title: @Title</h2>
<button class="btn btn-primary" @onclick="@GetTitle">Get Title</button>
@code {
public string Title = "";
    
    public async void GetTitle()
    {
            Title = await jsRuntime.InvokeAsync<string>("getTitle");
     }
}

چگونه می‌توان مسیری را از طریق کدهای برنامه در یک برگه‌ی مجزای مرورگر باز کرد؟
در اینجا نیز می‌توان با استفاده از JavaScript interop، متد استاندارد open مرورگر را فراخوانی کرد و پارامتر اول آن‌را به url مدنظر و پارامتر بعدی آن‌را به blank_ تنظیم کرد تا مرورگر آدرس درخواستی را در یک برگه‌ی جدید باز کند:
@inject IJSRuntime jsRuntime

<button @onclick="NavigateToNewTab">New Tab Navigation</button>
@code {
public async Task NavigateToNewTab()
    {
        string url = "/counter";
        await jsRuntime.InvokeAsync<object>("open", url, "_blank");
    }
}
مطالب
استفاده از MVVM زمانیکه امکان Binding وجود ندارد

ساده‌ترین تعریف MVVM، نهایت استفاده از امکانات Binding موجود در WPF و Silverlight است. اما خوب، همیشه همه چیز بر وفق مراد نیست. مثلا کنترل WebBrowser را در WPF در نظر بگیرید. فرض کنید که می‌خواهیم خاصیت Source آن‌را در ViewModel مقدار دهی کنیم تا صفحه‌ای را نمایش دهد. بلافاصله با خطای زیر متوقف خواهیم شد:

A 'Binding' cannot be set on the 'Source' property of type 'WebBrowser'.
A 'Binding' can only be set on a DependencyProperty of a DependencyObject.

بله؛ این خاصیت از نوع DependencyProperty نیست و نمی‌توان چیزی را به آن Bind کرد. بنابراین این نکته مهم را توسعه دهنده‌های کنترل‌های WPF و Silverlight همیشه باید بخاطر داشته باشند که اگر قرار است کنترل‌های شما MVVM friendly باشند باید کمی بیشتر زحمت کشیده و بجای تعریف خواص ساده دات نتی، خواص مورد نظر را از نوع DependencyProperty تعریف کنید.
الان که تعریف نشده چه باید کرد؟
پاسخ متداول آن این است: مهم نیست؛ خودمان می‌توانیم این‌کار را انجام دهیم! یک Attached property یا به عبارتی یک Behavior را تعریف و سپس به کمک آن عملیات Binding را میسر خواهیم ساخت. برای مثال:
در این Attached property قصد داریم یک خاصیت جدید به نام BindableSource را جهت کنترل WebBrowser تعریف کنیم:

using System;
using System.Windows;
using System.Windows.Controls;

namespace WebBrowserSample.Behaviors
{
public static class WebBrowserBehaviors
{
public static readonly DependencyProperty BindableSourceProperty =
DependencyProperty.RegisterAttached("BindableSource",
typeof(object),
typeof(WebBrowserBehaviors),
new UIPropertyMetadata(null, BindableSourcePropertyChanged));

public static object GetBindableSource(DependencyObject obj)
{
return (string)obj.GetValue(BindableSourceProperty);
}

public static void SetBindableSource(DependencyObject obj, object value)
{
obj.SetValue(BindableSourceProperty, value);
}

public static void BindableSourcePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WebBrowser browser = o as WebBrowser;
if (browser == null) return;

Uri uri = null;

if (e.NewValue is string)
{
var uriString = e.NewValue as string;
uri = string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString);
}
else if (e.NewValue is Uri)
{
uri = e.NewValue as Uri;
}

if (uri != null) browser.Source = uri;
}
}
}


یک مثال ساده از استفاده‌ی آن هم به صورت زیر می‌تواند باشد:
ابتدا ViewModel مرتبط با فرم برنامه را تهیه خواهیم کرد. اینجا چون یک خاصیت را قرار است Bind کنیم، همینجا داخل ViewModel آن‌را تعریف کرده‌ایم. اگر تعداد آن‌ها بیشتر بود بهتر است به یک کلاس مجزا مثلا GuiModel منتقل شوند.

using System;
using System.ComponentModel;

namespace WebBrowserSample.ViewModels
{
public class MainWindowViewModel : INotifyPropertyChanged
{
Uri _sourceUri;
public Uri SourceUri
{
get { return _sourceUri; }
set
{
_sourceUri = value;
raisePropertyChanged("SourceUri");
}
}

public MainWindowViewModel()
{
SourceUri = new Uri(@"C:\path\arrow.png");
}

#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

در ادامه بجای استفاده از خاصیت Source که قابلیت Binding ندارد، از Behavior سفارشی تعریف شده استفاده خواهیم کرد. ابتدا باید فضای نام آن تعریف شود، سپس BindableSource مرتبط آن در دسترس خواهد بود:

<Window x:Class="WebBrowserSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:VM="clr-namespace:WebBrowserSample.ViewModels"
xmlns:B="clr-namespace:WebBrowserSample.Behaviors"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<VM:MainWindowViewModel x:Key="vmMainWindowViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">
<WebBrowser B:WebBrowserBehaviors.BindableSource="{Binding SourceUri}" />
</Grid>
</Window>



نمونه مشابه این مورد را در مثال «استفاده از کنترل‌های Active-X در WPF» پیشتر در این سایت دیده‌اید.

مطالب
نحوه استفاده از افزونه Firebug برای دیباگ برنامه‌های ASP.NET مبتنی بر jQuery
هر از چندگاهی سؤال «این مثال jQuery رو نمی‌تونم اجرا یا باز سازی کنم» در این سایت یا سایت‌های مشابه تکرار می‌شوند. بنابراین بهتر است نحوه عیب یابی برنامه‌های ASP.NET مبتنی بر jQuery را یکبار با هم مرور کنیم. در اینجا، مثال تهیه یک Image Slider را که پیشتر در سایت مطرح شده است، به نحوی دیگر بررسی خواهیم کرد:
1) فراموش می‌کنیم تا اسکریپت اصلی jQuery را به درستی پیوست و مسیردهی کنیم.
2) مسیر Generic handler دیگری را ذکر می‌کنیم.
3) مسیرهای تصاویری را که Image slider باید نمایش دهد، کاملا بی‌ربط ذکر می‌کنیم.
4) خروجی JSON نامربوطی را بازگشت می‌دهیم.
5) یکبار هم یک استثنای عمدی دستی را در بین کدها قرار خواهیم داد.

و ... بعد سعی می‌کنیم با استفاده از Firebug عیوب فوق را یافته و اصلاح کنیم؛ تا به یک برنامه قابل اجرا برسیم.


معرفی برنامه‌ای که کار نمی‌کند!

یک برنامه ASP.NET Empty web application را آغاز کنید. سپس سه پوشه Scripts، Content و Images را به آن اضافه نمائید. در این پوشه‌ها، اسکریپت‌های نمایش دهنده تصاویر، Css آن و تصاویری که قرار است نمایش داده شوند، قرار می‌گیرند:


سپس یک فایل default.aspx و یک فایل OrbitHandler.ashx را نیز به پروژه با محتویات ذیل اضافه کنید: (در این دو فایل، 5 مورد مشکل ساز یاد شده لحاظ شده‌اند)
محتویات فایل OrbitHandler.ashx.cs مطابق کدهای ذیل است:
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Script.Serialization;

namespace OrbitWebformsTest
{
    public class Picture
    {
        public string Title { set; get; }
        public string Path { set; get; }
    }

    public class OrbitHandler : IHttpHandler
    {
        IList<Picture> PicturesDataSource()
        {
            var results = new List<Picture>();
            var path = HttpContext.Current.Server.MapPath("~/Images");

            foreach (var item in Directory.GetFiles(path, "*.*"))
            {
                var name = Path.GetFileName(item);
                results.Add(new Picture
                {
                    Path = /*"Images/" + name*/ name,
                    Title = name
                });
            }

            return results;
        }

        public void ProcessRequest(HttpContext context)
        {
            var items = PicturesDataSource();
            var json = /*new JavaScriptSerializer().Serialize(items)*/ string.Empty;
            throw new InvalidDataException("همینطوری");
            context.Response.ContentType = "text/plain";
            context.Response.Write(json);
        }

        public bool IsReusable
        { 
            get { return false; } 
        }
    }
}
در اینجا جهت سهولت دموی برنامه (و همچنین امکان باز تولید آن توسط خوانندگان)، از بانک اطلاعاتی استفاده نشده و عمدا از یک لیست جنریک تشکیل شده در حافظه کمک گرفته شده است. تصاویر برنامه در پوشه Images واقع در ریشه سایت، قرار دارند. بنابراین توسط متد PicturesDataSource، فایل‌های این پوشه را یافته و مطابق ساختار کلاس Picture بازگشت می‌دهیم. نهایتا این اطلاعات به ظاهر قرار است با فرمت JSON بازگشت داده شوند تا بتوان نتیجه را توسط افزونه Orbit استفاده کرد.

همچنین کدهای صفحه ASPX ایی که قرار است (به ظاهر البته) از این Generic handler استفاده کند به نحو ذیل است:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="OrbitWebformsTest._default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <link href="Content/orbit-1.2.3.css" rel="stylesheet" type="text/css" />
    <script src="Script/jquery-1.5.1.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.orbit-1.2.3.min.js" type="text/javascript"></script>
</head>
<body>
    <form id="form1" runat="server">
    <div id="featured">
    </div>
    </form>
    <script type="text/javascript">
        $(function () {
            $.ajax({
                url: "Handler.ashx",
                contentType: "application/json; charset=utf-8",
                success: function (data) {
                    $.each(data, function (i, b) {
                        var str = '<img src="' + b.Path + '" alt="' + b.Title + '"/>';
                        $("#featured").append(str);
                    });
                    $('#featured').orbit();
                },
                dataType: "json"
            });
        });
    </script>
</body>
</html>
خوب! اگر پروژه را اجرا کنیم، کار نمی‌کند. یک مستطیل مشکی رنگ در کنار صفحه ظاهر شده و همین! حالا چکار باید کرد؟


مراحل عیب یابی برنامه‌ای که کار نمی‌کند!

ابتدا برنامه را در فایرفاکس باز کرده و سپس افزونه Firebug را با کلیک بر روی آیکن آن، بر روی سایت فعال می‌کنیم. سپس یکبار بر روی دکمه F5 کلیک کنید تا مجددا مراحل بارگذاری سایت تحت نظر افزونه Firebug فعال شده، طی شود.


اولین موردی که مشهود است، نمایش عدد 3، کنار آیکن فایرباگ می‌باشد. این عدد به معنای وجود خطاهای اسکریپتی در کدهای ما است.
برای مشاهده این خطاها، بر روی برگه Console آن کلیک کنید: 


بله. مشخص است که مسیر دهی فایل jquery-1.5.1.min.js صحیح نبوده و همین مساله سبب بروز خطاهای اسکریپتی گردیده است. برای اصلاح آن سطر زیر را در برنامه تغییر دهید:
 <script src="Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
پیشتر پوشه Script ذکر شده بود که باید تبدیل به Scripts شود.

مجددا دکمه F5 را فشرده و سایت را با تنظیمات جدید اجرا کنید. اینبار در برگه Console و یا در برگه شبکه فایرباگ، خطای یافت نشدن Generic handler نمایان می‌شوند:


برای رفع آن به فایل default.aspx مراجعه و بجای معرفی Handler.ashx، نام OrbitHandler.ashx را وارد کنید.
مجددا دکمه F5 را فشرده و سایت را با تنظیمات جدید اجرا کنید.


اگر به برگه کنسول دقت کنیم، بروز استثناء در کدها تشخیص داده شده و همچنین در برگه Response پاسخ دریافتی از سرور، جزئیات صفحه خطای بازگشتی از آن نیز قابل بررسی و مشاهده است.
اینبار به فایل OrbitHandler.ashx.cs مراجعه کرده و سطر throw new InvalidDataException را حذف می‌کنیم. در ادامه برنامه را کامپایل و مجددا اجرا خواهیم کرد.



با اجرای مجدد سایت، تبادل اطلاعات صحیحی با فایل OrbitHandler.ashx برقرار شده است، اما خروجی خاصی قابل مشاهده نیست. بنابراین بازهم سایت کار نمی‌کند.
برای رفع این مشکل، متد ProcessRequest را به نحو ذیل تغییر خواهیم داد:
        public void ProcessRequest(HttpContext context)
        {
            var items = PicturesDataSource();
            var json = new JavaScriptSerializer().Serialize(items);            
            context.Response.ContentType = "text/plain";
            context.Response.Write(json);
        }
برنامه را کامپایل کرده و اجرا می‌کنیم. برنامه اجرا می‌شود، اما باز هم کار نمی‌کند. مشکل از کجاست؟


بله. تمام تنظیمات به نظر درست هستند، اما در برگه شبکه فایرباگ تعدادی خطای 404 و یا «یافت نشد»، مشاهده می‌شوند. مشکل اینجا است که مسیرهای بازگشت داده شده توسط متد Directory.GetFiles، مسیرهای مطلقی هستند؛ مانند c:\path\images\01.jpg و جهت نمایش در یک وب سایت مناسب نمی‌باشند. برای تبدیل آن‌ها به مسیرهای نسبی، اینبار کدهای متد تهیه منبع داده را به نحو ذیل ویرایش می‌کنیم:
        IList<Picture> PicturesDataSource()
        {
            var results = new List<Picture>();
            var path = HttpContext.Current.Server.MapPath("~/Images");

            foreach (var item in Directory.GetFiles(path, "*.*"))
            {
                var name = Path.GetFileName(item);
                results.Add(new Picture
                {
                    Path = "Images/" + name,
                    Title = name
                });
            }

            return results;
        }
در این کدها فقط قسمت Path ویرایش شده است تا به مسیر پوشه Images واقع در ریشه سایت اشاره کند.
اینبار اگر برنامه را اجرا کنیم، بدون مشکل کار خواهد کرد.

بنابراین در اینجا مشاهده کردیم که اگر «برنامه‌ای مبتنی بر jQuery کار نمی‌کند»، چگونه باید قدم به قدم با استفاده از فایرباگ و امکانات آن، به خطاهایی که گزارش می‌دهد و یا مسیرهایی را که یافت نشد بیان می‌کند، دقت کرد تا بتوان برنامه را عیب یابی نمود.


سؤال مهم: اجرای کدهای jQuery Ajax فوق، چه تغییری را در صفحه سبب می‌شوند؟

اگر به برگه اسکریپت‌ها در کنسول فایرباگ مراجعه کنیم، امکان قرار دادن breakpoint بر روی سطرهای کدهای جاوا اسکریپتی نمایش داده شده نیز وجود دارد:


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