نظرات مطالب
لیست تازه‌های IIS 7.5
IIS یک کامپوننت ویندوز است و جدا از ویندوز ارائه نمی‌شود.
مطالب
7# آموزش سیستم مدیریت کد Git : بازیابی فایل

در این مقاله با یکی از مهمترین ویژگی‌های git یعنی بازیابی تغییرات فایل‌ها، آشنا می‌شویم. اما در ابتدا نگاهی می‌کنیم به چگونگی ایجاد تغییر در آخرین commit:


تغییر آخرین commit:
در git این امکان وجود دارد که آخرین فرمان commit با استفاده از اصلاح‌کننده amend تغییر کند. علت تاکید بر روی آخرین دستور این است که git به دلیل ساختاری که دارد نمی‌تواند commit‌های قبل را تغییر دهد. اگر مقالات ابتدایی آموزش git را مطالعه کرده باشید، به خاطر دارید که هر commit دارای یک کد منحصر به فرد SHA-1 است، که این کد از هش کردن BLOBها به همراه خود مقادیر commit یعنی مشخصات ایجاد کننده آن و از همه مهمتر SHA-1 پدر ایجاد می‌شود. در نتیجه تغییر commitیی که نقش برگ را ندارد، یعنی در ساختار درختی git دارای فرزند است، سبب می‌شود کد SHA-1 آن تغییر کند. این تغییر، commit‌های فرزند را مجاب می‌کند برای حفظ صحت داده‌ها مقدار SHA-1 خود را تغییر دهند. به این ترتیب این تغییرات در کل repository پخش خواهد شد. به همین دلیل git جز آخرین commit امکان اصلاح دیگر commit‌ها را نخواهد داد.
برای اصلاح آخرین commit کافی است دستور commit خود را با amend-- بیاورید

دستورات بازیابی فایل:

دستور checkout:
این فرمان یکی از مهمترین فرمان‌های git است که دارای دو کاربرد است:
۱) بازیابی فایلی از repository و یا stage
۲) تغییر شاخه (این مورد را در مقالات مربوط به branch بررسی خواهیم کرد)
با استفاده از این دستور می‌توان فایلی را از repository به stage یا working tree و یا هر دو بیاوریم. عملکرد این دستور با اصلاح کننده‌های گوناگون متفاوت خواهد بود. در ادامه روش‌های مختلف فراخوانی این دستور و کاربرد هر کدام آورده شده است:
در صورتی که بخواهیم فایلی را از محلی که head اکنون به آن اشاره می‌کند به working tree بیاوریم از دستور زیر استفاده می‌کنیم:

git checkout --[filename]

در حالت فوق فایل مستقیما به working tree آورده شده و در stage قرار نمی‌گیرد

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

git checkout [SHA-1] [filename]

در این حالت فایل هم در stage و هم در working tree قرار می‌گیرد.



دستور reset:
در صورتی‌که بخواهید تعداد زیادی فایل را به وضعیت مشخصی در زمان قبل برگدانید، reset فرمان مناسبی خواهد بود. البته استفاده از این دستور باید با احتیاط کامل صورت گیرد. زیرا در صورت اشتباه، این امکان وجود دارد که دیگر نتوانید به بخشی از سوابق فایل‌های خود دسترسی داشته باشید. بنابراین این دستور همان‌قدر که کاربردی است، به همان اندازه نیز خطرناک است.
دستور reset را می‌توان به ۳ صورت اجرا نمود:
۱) soft
۲) mixed (حالت پیشفرض)
۳) hard
۱)در حالت soft تنها head به commit گفته شده منتقل می‌شود و working tree و همچنین stage تغییری نمی‌کند. دقیقا مانند آن‌که هد یک نوار خوان ویدئویی به جای آن‌که به آخرین محل ضبط اشاره کند، به عقب برگشته و به قسمتی در قبل برود. در این حالت در صورتی‌که دستور commit جدیدی ایجاد نشود که باعث پاک شدن  commitهای از آن‌جا به بعد شود، می‌توان با اجرای مجدد دستور reset و اشاره به آخرین commit، مجددا head را به سر جای اول برگرداند. البته توجه کنید در صورتی‌که در هنگام برگرداندن head به commit‌های قبلی، فایل‌هایی تغییر کرده باشند، آن‌ها به صورت خودکار به stage اضافه می‌شوند.
۲) در حالت mixed که پیش فرض این دستور نیز است، working tree بدون تغییر می‌ماند. اما stage تغییر کرده و دقیقا مانند وضعیت commit می‌شود.
۳) در این حالت هم working tree و هم stage تغییر می‌کند و عینا وضعیت commitیی را می‌گیرند که اکنون head به آن اشاره می‌کند. استفاده از این اصلاح کننده بسیار خطرناک‌تر از موارد قبل است.
در هر یک از موارد فوق تا زمانی‌که دستور commit جدیدی را اجرا نکرده باشید، می‌توانید به وضعیت قبل برگردید. اما اگر commit جدید اجرا شود دیگر امکان بازگشت به commit‌های صورت گرفته بعد از آن وجود ندارد.

نکته مهم:

علیرغم آن‌که می‌توان به commit‌های گذشته در صورت عدم داشتن commit جدید مراجعه کرد، اما یک اشکال فنی وجود دارد و آن این است که شما نمی‌توانید SHA-1‌های آن commitها را با دستوراتی نظیر log ببینید. بنابراین بهتر است مقدار آن‌ها را قبل از اجرای دستور، ذخیره و تا اطمینان از وضعیت فعلی در محلی نگه دارید.

شکل زیر نمایانگر وضعیت‏‌های مختلف دستور reset در هنگام بازگشت به سه commit قبل نسبت به وضعیت فعلی Head است:



 

مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت سوم

در قسمت قبلی بروز رسانی موجودیت‌های منفصل با WCF را بررسی کردیم. در این قسمت خواهیم دید چگونه می‌توان تغییرات موجودیت‌ها را تشخیص داد و عملیات CRUD را روی یک Object Graph اجرا کرد.

تشخیص تغییرات با Web API

فرض کنید می‌خواهیم از سرویس‌های Web API برای انجام عملیات CRUD استفاده کنیم، اما بدون آنکه برای هر موجودیت متدهایی مجزا تعریف کنیم. به بیان دیگر می‌خواهیم عملیات مذکور را روی یک Object Graph انجام دهیم. مدیریت داده‌ها هم با مدل Code-First پیاده سازی می‌شود. در مثال جاری یک اپلیکیشن کنسول خواهیم داشت که بعنوان یک کلاینت سرویس را فراخوانی می‌کند. هر پروژه نیز در Solution مجزایی قرار دارد، تا یک محیط n-Tier را شبیه سازی کنیم.

مدل زیر را در نظر بگیرید.

همانطور که می‌بینید مدل ما آژانس‌های مسافرتی و رزرواسیون آنها را ارائه می‌کند. می‌خواهیم مدل و کد دسترسی داده‌ها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید:

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe3.Service تغییر دهید.
  • کنترلر جدیدی بنام TravelAgentController به پروژه اضافه کنید.
  • دو کلاس جدید با نام‌های TravelAgent و Booking بسازید و کد آنها را مطابق لیست زیر تغییر دهید.
public class TravelAgent
{
    public TravelAgent()
    {
        this.Bookings = new HashSet<Booking>();
    }

    public int AgentId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Booking> Bookings { get; set; }
}

public class Booking
{
    public int BookingId { get; set; }
    public int AgentId { get; set; }
    public string Customer { get; set; }
    public DateTime BookingDate { get; set; }
    public bool Paid { get; set; }
    public virtual TravelAgent TravelAgent { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • کلاس جدیدی بنام Recipe3Context بسازید و کد آن را مطابق لیست زیر تغییر دهید.
public class Recipe3Context : DbContext
{
    public Recipe3Context() : base("Recipe3ConnectionString") { }
    public DbSet<TravelAgent> TravelAgents { get; set; }
    public DbSet<Booking> Bookings { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TravelAgent>().HasKey(x => x.AgentId);
        modelBuilder.Entity<TravelAgent>().ToTable("TravelAgents");
        modelBuilder.Entity<Booking>().ToTable("Bookings");
    }
}

  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه کنید.
<connectionStrings>
  <add name="Recipe3ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Model Compatibility در EF را غیرفعال می‌کند. همچنین به JSON serializer می‌گوییم که self-referencing loop خاصیت‌های پیمایشی را نادیده بگیرد. این حلقه بدلیل ارتباط bidirectional بین موجودیت‌ها بوجود می‌آید.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);

    // The bidirectional navigation properties between related entities
    // create a self-referencing loop that breaks Web API's effort to
    // serialize the objects as JSON. By default, Json.NET is configured
    // to error when a reference loop is detected. To resolve problem,
    // simply configure JSON serializer to ignore self-referencing loops.
    GlobalConfiguration.Configuration.Formatters.JsonFormatter
        .SerializerSettings.ReferenceLoopHandling =
        Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    ...
}
  • فایل RouteConfig.cs را باز کنید و قوانین مسیریابی را مانند لیست زیر تغییر دهید.
public static void Register(HttpConfiguration config)
{
    config.Routes.MapHttpRoute(
      name: "ActionMethodSave",
      routeTemplate: "api/{controller}/{action}/{id}",
      defaults: new { id = RouteParameter.Optional });
}
  • در آخر کنترلر TravelAgent را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
public class TravelAgentController : ApiController
{
    // GET api/travelagent
    [HttpGet]
    public IEnumerable<TravelAgent> Retrieve()
    {
        using (var context = new Recipe3Context())
        {
            return context.TravelAgents.Include(x => x.Bookings).ToList();
        }
    }

    /// <summary>
    /// Update changes to TravelAgent, implementing Action-Based Routing in Web API
    /// </summary>
    public HttpResponseMessage Update(TravelAgent travelAgent)
    {
        using (var context = new Recipe3Context())
        {
            var newParentEntity = true;
            // adding the object graph makes the context aware of entire
            // object graph (parent and child entities) and assigns a state
            // of added to each entity.
            context.TravelAgents.Add(travelAgent);
            if (travelAgent.AgentId > 0)
            {
                // as the Id property has a value greater than 0, we assume
                // that travel agent already exists and set entity state to
                // be updated.
                context.Entry(travelAgent).State = EntityState.Modified;
                newParentEntity = false;
            }

            // iterate through child entities, assigning correct state.
            foreach (var booking in travelAgent.Bookings)
            {
                if (booking.BookingId > 0)
                    // assume booking already exists if ID is greater than zero.
                    // set entity to be updated.
                    context.Entry(booking).State = EntityState.Modified;
            }

            context.SaveChanges();
            HttpResponseMessage response;
            // set Http Status code based on operation type
            response = Request.CreateResponse(newParentEntity ? HttpStatusCode.Created : HttpStatusCode.OK, travelAgent);
            return response;
        }
    }

    [HttpDelete]
    public HttpResponseMessage Cleanup()
    {
        using (var context = new Recipe3Context())
        {
            context.Database.ExecuteSqlCommand("delete from [bookings]");
            context.Database.ExecuteSqlCommand("delete from [travelagents]");
        }
        return Request.CreateResponse(HttpStatusCode.OK);
    }
}

در قدم بعدی کلاینت پروژه را می‌سازیم که از سرویس Web API مان استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console application بسازید و نام آن را به Recipe3.Client تغییر دهید.
  • فایل program.cs را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
internal class Program
{
    private HttpClient _client;
    private TravelAgent _agent1, _agent2;
    private Booking _booking1, _booking2, _booking3;
    private HttpResponseMessage _response;

    private static void Main()
    {
        Task t = Run();
        t.Wait();
        Console.WriteLine("\nPress <enter> to continue...");
        Console.ReadLine();
    }

    private static async Task Run()
    {
        var program = new Program();
        program.ServiceSetup();
        // do not proceed until clean-up is completed
        await program.CleanupAsync();
        program.CreateFirstAgent();
        // do not proceed until agent is created
        await program.AddAgentAsync();
        program.CreateSecondAgent();
        // do not proceed until agent is created
        await program.AddSecondAgentAsync();
        program.ModifyAgent();
        // do not proceed until agent is updated
        await program.UpdateAgentAsync();
        // do not proceed until agents are fetched
        await program.FetchAgentsAsync();
    }

    private void ServiceSetup()
    {
        // set up infrastructure for Web API call
        _client = new HttpClient {BaseAddress = new Uri("http://localhost:6687/")};
        // add Accept Header to request Web API content negotiation to return resource in JSON format
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

    private async Task CleanupAsync()
    {
        // call cleanup method in service
        _response = await _client.DeleteAsync("api/travelagent/cleanup/");
    }

    private void CreateFirstAgent()
    {
        // create new Travel Agent and booking
        _agent1 = new TravelAgent {Name = "John Tate"};
        _booking1 = new Booking
        {
            Customer = "Karen Stevens",
            Paid = false,
            BookingDate = DateTime.Parse("2/2/2010")
        };

        _booking2 = new Booking
        {
            Customer = "Dolly Parton",
            Paid = true,
            BookingDate = DateTime.Parse("3/10/2010")
        };
  
        _agent1.Bookings.Add(_booking1);
        _agent1.Bookings.Add(_booking2);
    }

    private async Task AddAgentAsync()
    {
        // call generic update method in Web API service to add agent and bookings
        _response = await _client.PostAsync("api/travelagent/update/",
            _agent1, new JsonMediaTypeFormatter());

        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service, which will include
            // database-generated Ids for each entity
            _agent1 = await _response.Content.ReadAsAsync<TravelAgent>();
            _booking1 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Karen Stevens");
            _booking2 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Dolly Parton");

            Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)",
            _agent1.Name, _agent1.Bookings.Count);
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }

    private void CreateSecondAgent()
    {
        // add new agent and booking
        _agent2 = new TravelAgent {Name = "Perry Como"};
        _booking3 = new Booking {
            Customer = "Loretta Lynn",
            Paid = true,
            BookingDate = DateTime.Parse("3/15/2010")};
        _agent2.Bookings.Add(_booking3);
    }

    private async Task AddSecondAgentAsync()
    {
        // call generic update method in Web API service to add agent and booking
        _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter());

        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service
            _agent2 = await _response.Content.ReadAsAsync<TravelAgent>();
            _booking3 = _agent2.Bookings.FirstOrDefault(x => x.Customer == "Loretta Lynn");
            Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)",
                _agent2.Name, _agent2.Bookings.Count);
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }

    private void ModifyAgent()
    {
        // modify agent 2 by changing agent name and assigning booking 1 to him from agent 1
        _agent2.Name = "Perry Como, Jr.";
        _agent2.Bookings.Add(_booking1);
    }

    private async Task UpdateAgentAsync()
    {
        // call generic update method in Web API service to update agent 2
        _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service, which will include Ids
            _agent1 = _response.Content.ReadAsAsync<TravelAgent>().Result;
            Console.WriteLine("Successfully updated Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count);
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }

    private async Task FetchAgentsAsync()
    {
        // call Get method on service to fetch all Travel Agents and Bookings
        _response = _client.GetAsync("api/travelagent/retrieve").Result;
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service, which will include Ids
            var agents = await _response.Content.ReadAsAsync<IEnumerable<TravelAgent>>();

            foreach (var agent in agents)
            {
                Console.WriteLine("Travel Agent {0} has {1} Booking(s)", agent.Name, agent.Bookings.Count());
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }
}
  • در آخر کلاس‌های TravelAgent و Booking را به پروژه کلاینت اضافه کنید. اینگونه کدها بهتر است در لایه مجزایی قرار گیرند و بین پروژه‌ها به اشتراک گذاشته شوند.

اگر اپلیکیشن کنسول (کلاینت) را اجرا کنید با خروجی زیر مواجه خواهید شد.

(Successfully created Travel Agent John Tate and 2 Booking(s
(Successfully created Travel Agent Perry Como and 1 Booking(s
(Successfully updated Travel Agent Perry Como, Jr. and 2 Booking(s
(Travel Agent John Tate has 1 Booking(s
(Travel Agent Perry Como, Jr. has 2 Booking(s


شرح مثال جاری

با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت می‌کند. در این مرحله سایت در حال اجرا است و سرویس‌ها قابل دسترسی هستند.

سپس اپلیکیشن کنسول را باز کنید، روی خط اول کد فایل program.cs یک breakpoint قرار دهید و آن را اجرا کنید. ابتدا آدرس سرویس Web API را نگاشت می‌کنیم و با تنظیم مقدار خاصیت Accept Header از سرویس درخواست می‌کنیم که اطلاعات را با فرمت JSON بازگرداند.

بعد از آن با استفاده از آبجکت HttpClient متد DeleteAsync را فراخوانی می‌کنیم که روی کنترلر TravelAgent تعریف شده است. این متد تمام داده‌های پیشین را حذف میکند.

در قدم بعدی سه آبجکت جدید می‌سازیم: یک آژانس مسافرتی و دو رزرواسیون. سپس این آبجکت‌ها را با فراخوانی متد PostAsync روی آبجکت HttpClient به سرویس ارسال می‌کنیم. اگر به متد Update در کنترلر TravelAgent یک breakpoint اضافه کنید، خواهید دید که این متد آبجکت آژانس مسافرتی را بعنوان یک پارامتر دریافت می‌کند و آن را به موجودیت TravelAgents در Context جاری اضافه می‌نماید. این کار آبجکت آژانس مسافرتی و تمام آبجکت‌های فرزند آن را در حالت Added اضافه می‌کند و باعث می‌شود که context جاری شروع به ردیابی (tracking) آنها کند.

نکته: قابل ذکر است که اگر موجودیت‌های متعددی با مقداری یکسان در خاصیت کلید اصلی (Primary-key value) دارید باید مجموعه آبجکت‌های خود را Add کنید و نه Attach. در مثال جاری چند آبجکت Booking داریم که مقدار کلید اصلی آنها صفر است (Bookings with Id = 0). اگر از Attach استفاده کنید EF پیغام خطایی صادر می‌کند چرا که چند موجودیت با مقادیر کلید اصلی یکسان به context جاری اضافه کرده اید.

بعد از آن بر اساس مقدار خاصیت Id مشخص می‌کنیم که موجودیت‌ها باید بروز رسانی شوند یا خیر. اگر مقدار این فیلد بزرگتر از صفر باشد، فرض بر این است که این موجودیت در دیتابیس وجود دارد بنابراین خاصیت EntityState را به Modified تغییر می‌دهیم. علاوه بر این فیلدی هم با نام newParentEntity تعریف کرده ایم که توسط آن بتوانیم کد وضعیت مناسبی به کلاینت بازگردانیم. در صورتی که مقدار فیلد Id در موجودیت TravelAgent برابر با یک باشد، مقدار خاصیت EntityState را به همان Added رها می‌کنیم.

سپس تمام آبجکت‌های فرزند آژانس مسافرتی (رزرواسیون ها) را بررسی میکنیم و همین منطق را روی آنها اعمال می‌کنیم. یعنی در صورتی که مقدار فیلد Id آنها بزرگتر از 0 باشد وضعیت EntityState را به Modified تغییر می‌دهیم. در نهایت متد SaveChanges را فراخوانی می‌کنیم. در این مرحله برای موجودیت‌های جدید اسکریپت‌های Insert و برای موجودیت‌های تغییر کرده اسکریپت‌های Update تولید می‌شود. سپس کد وضعیت مناسب را به کلاینت بر می‌گردانیم. برای موجودیت‌های اضافه شده کد وضعیت 201 (Created) و برای موجودیت‌های بروز رسانی شده کد وضعیت 200 (OK) باز می‌گردد. کد 201 به کلاینت اطلاع می‌دهد که رکورد جدید با موفقیت ثبت شده است، و کد 200 از بروز رسانی موفقیت آمیز خبر می‌دهد. هنگام تولید سرویس‌های REST-based بهتر است همیشه کد وضعیت مناسبی تولید کنید.

پس از این مراحل، آژانس مسافرتی و رزرواسیون جدیدی می‌سازیم و آنها را به سرویس ارسال می‌کنیم. سپس نام آژانس مسافرتی دوم را تغییر می‌دهیم، و یکی از رزرواسیون‌ها را از آژانس اولی به آژانس دومی منتقل می‌کنیم. اینبار هنگام فراخوانی متد Update تمام موجودیت‌ها شناسه ای بزرگتر از 1 دارند، بنابراین وضعیت EntityState آنها را به Modified تغییر می‌دهیم تا هنگام ثبت تغییرات دستورات بروز رسانی مناسب تولید و اجرا شوند.

در آخر کلاینت ما متد Retreive را روی سرویس فراخوانی می‌کند. این فراخوانی با کمک متد GetAsync انجام می‌شود که روی آبجکت HttpClient تعریف شده است. فراخوانی این متد تمام آژانس‌های مسافرتی بهمراه رزرواسیون‌های متناظرشان را دریافت می‌کند. در اینجا با استفاده از متد Include تمام رکوردهای فرزند را بهمراه تمام خاصیت هایشان (properties) بارگذاری می‌کنیم.

دقت کنید که مرتب کننده JSON تمام خواص عمومی (public properties) را باز می‌گرداند، حتی اگر در کد خود تعداد مشخصی از آنها را انتخاب کرده باشید.

نکته دیگر آنکه در مثال جاری از قرارداد‌های توکار Web API برای نگاشت درخواست‌های HTTP به اکشن متدها استفاده نکرده ایم. مثلا بصورت پیش فرض درخواست‌های POST به متدهایی نگاشت می‌شوند که نام آنها با "Post" شروع می‌شود. در مثال جاری قواعد مسیریابی را تغییر داده ایم و رویکرد مسیریابی RPC-based را در پیش گرفته ایم. در اپلیکیشن‌های واقعی بهتر است از قواعد پیش فرض استفاده کنید چرا که هدف Web API ارائه سرویس‌های REST-based است. بنابراین بعنوان یک قاعده کلی بهتر است متدهای سرویس شما به درخواست‌های متناظر HTTP نگاشت شوند. و در آخر آنکه بهتر است لایه مجزایی برای میزبانی کدهای دسترسی داده ایجاد کنید و آنها را از سرویس Web API تفکیک نمایید.

نظرات مطالب
سازماندهی برنامه‌های Angular توسط ماژول‌ها
سلام ؛ اگر قرار باشد کلیه روتهای برنامه به عنوان childrout‌های والدی مثلا pages باشند از آنجا که برنامه حاوی چندین feature modules است تعریف همه  روتها در app-routing باشد ؟ یا در featureX-routing  ؟ (روت والد دارای کامپوننت page-component می‌باشد و منظور گروه بندی  تنها نیست . ) با تشکر .
مطالب
طراحی گزارش در Stimulsoft Reports.Net – بخش 1

برای طراحی گزارش شما میتوانید به سه روش این کار را انجام دهید.

1- طراحی در برنامه طراح گزارش

2- طراحی از داخل ویژوال استودیو

3- طراحی گزارش در زمان اجرا

برای شروع شما میتوانید نسخه آزمایشی این گزارش‌ساز را دریافت کنید. تنها محدودیت این نسخه نمایش عبارت Demo در چاپ میباشد.

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

Header, Footer, Data, Page Header, Page Footer, Report Title, Report Summery

*به ابزارهای بالا Band گفته میشود.

Header , Footer :

همانطور که از نامشان پیداست در قسمت بالا و پایین بخشی از گزارش قرار میگیرند که برای استفاده در بالا و پایین بند Data میباشد. به عنوان مثال بند Header مناسب طراحی سرستونهای یک جدول میباشد و بند Footer  هم جهت نمایش اطلاعات انتهایی یک جدول. ولی شما میتوانید با تنظیم خصوصیات هر بند رفتار و نمایش آنها را به طور کل تغییر دهید. نکته مثبت این گزارش‌ساز این است که شما میتوانید بیش از یک واحد از هر بند را بر روی صفحه طراح خود قرار دهید، به عنوان مثال شما میتوانید دو بند Header داشته باشید که یکی در صفحات زوج و دیگری در صفحات فرد نمایش داده شود.

Data :

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

Page Header , Page Footer :

این دو بند با توجه به نامشان جهت نمایش در بالا و پایین هر صفحه از گزارش میباشد. البته باز هم یادآور میشوم که با تغییر در خصوصیات‌شان میتوانید رفتار و نحوه نمایش آنها را تغییر دهید.

Report Title :

این بند فقط در ابتدای گزارش نمایش داده خواهد شد.

Report Summery :

این بند بلافاصله بعد از اتمام گزارش نمایش داده خواهد شد.

مثال :

برای شروع در Designer یک گزارش جدید از نوع Blank Report ایجاد نمایید. سپس در پنل Dictionary بر روی New Item کلیک کرده و گزینه XML Data را انتخاب نمایید. با توجه به محل نصب گزارش‌ساز وارد مسیر …\Bin\Data شده و فایلهای Demo.xsd و Demo.xml را برای قسمتهای مربوطه انتخاب نمایید. یک بار دیگر بر رو New Item کلیک کرده و گزینه New Data Source را انتخاب نمایید، از لیست ظاهر شده کانکشنی را که ایجاد کرده‌اید را انتخاب نمایید؛ نتیجه کار تا اینجا باید به صورت زبر باشد.

جدول Product را دراگ کرده و بر روی صفحه طراحی گزارش رها کنید. فرم Data ظاهر میشود این فرم را مطابق تصویر زیر تنظیم نمایید.

حال بر روی صفحه طراحی گزارش بندهای Header, Data, Footer مشاهده میشود؛ حال شما میتوانید با کلیک بر روی سربرگ Preview خروجی گزارش را ببینید.

توابع :

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

در این بخش ما از توابع موجود در گزارش استفاده خواهیم کرد. برای شروع بر روی کامپوننت Text در بند Footer  زیر ستون UnitPrice دابل کلیک کرده تا فرم TextEditor ظاهر شود. سربرگ Summery را انتخاب نمایید. مطابق اطلاعات زیر بخشها را تنظیم نمایید.

Summery Function: Sum

Data Band: DataProducts

Data Column: Products.UnitPrice

حال بر روی سربرگ Preview کلیک نمایید تا خروجی گزارش را ببینید. جمع ستون UnitPrice فقط در صفحه آخر نمایش داده خواهد شد. اگر بخواهید جمع ستون در پایین هر صفحه نمایش داده شود ابتدا باید خصوصیت Print on All Pages بند Footer به True ست شود. سپس بر روی کامپوننت Text در بند Footer، دابل کلیک نمایید و در فرم TextEditor سربرگ Summery تیک Running Total را به حالت انتخاب شده در بیاورید، حال خروجی گزارش را ببینید، جمع در انتهای هر صفحه ظاهر میشود.

متغیرها :

در این گزارش ساز دو نوع متغیر وجود دارد؛ نرمال و سیستمی. نوع سیستمی شامل متغیرهایی میشود که کاربرد مشخصی در تهیه گزارش دارند، مثل شماره صفحه، شماره ردیف، عنوان گزارش و ...

برای مثال شما میتوانید متغیر سیستمی Line را برای روی صفحه طراحی دراگ کنید. دو کامپوننت Text بر روی صفحه ایجاد میشود. اولی با محتوای Line و دومی با محتوای {Line}. اولی را به بند Header و دومی را به بند Data منتقل کنید و سپس خروجی گزارش را مشاهده نمایید، حال گزارش شما دارای شماره ردیف است.

متغیرهای نرمال تقریبا همانند متغیرهایی هستند که همه روزه شما در برنامه‌های خود از آنها استفاده می‌کنید. با کلیک بر روی New Item گزینه New Variable را انتخاب نمایید و نوع متغیر را Decimal انتخاب نمایید، سپس متغیر ایجاد شده را دراگ کرده و بروی صفحه طراحی قرار دهید و مشابه متغیر Line عمل کرده و کامپوننتهای Text را در بندهای مناسب قرار دهید. سپس بند Data بر روی صفحه طراحی را انتخاب نمایید، در پنل Properties بر روی Eventes کلیک کرده سپس در رویداد Rendering کد زیر را وارد نمایید.

Variable1 += Products.UnitPrice

حال در خروجی گزارش میتوانید مقادیر محاسبه شده را ببینید. توجه داشته باشید که شما میتوانید در رویدادهای این گزارش‌ساز به زبان VB و C# برنامه نویسی کنید و محدود به یک خط کد نمی‌باشید.

شما میتوانید گزارش ساخته شده را به صورتهای مختلف ذخیره کنید از جمله کد C# و یا یک اپلیکیشن قابل اجرا. 

Report.mrt 

مطالب
چگونه نرم افزارهای تحت وب سریعتری داشته باشیم؟ قسمت سوم
قسمت دوم 

8.ORM Lazy Load
در هنگام استفاده از ORM‌ها دقت کنید کجا از Lazy Load استفاده می‌کنید. Lazy Load باعث می‌شود وقتی شما اطلاعات مرتبط را از بانک اطلاعات واکشی می‌کنید، این واکشی اطلاعات در چند query از بانک انجام شود. درعوض عدم استفاده از Lazy Load باعث می‌شود تمامی اطلاعات مورد نیاز شما در یک query از بانک اطلاعاتی دریافت شود. این موضوع یعنی سربار کمتر در شبکه، در بانک اطلاعاتی، در منابع حافظه و منابع پر ازرش cpu در سرورها. البته استفاده از include در حالت فعال بودن یا نبودن lazy هم داستان مجزایی دارد که اگر عمری باقی باشد راجع به آن مقاله ای خواهم نوشت.
 به این نمونه دقت کنید:
List<Customer> customers = context.Customers.ToList();
foreach (Customer cust in context.Customers){
   Console.WriteLine("Customer {0}, Account {1}", cust.Person.LastName.Trim() + ", " + cust.Person.FirstName, cust.AccountNumber); 
}
همچین کدی (در صورت فعال بودن Lazy Load در ORM) در صورتی که جدول Customers دارای 1000 رکورد باشد، باعث می‌شود برنامه 1001 دستور sql تولید و در بانک اجرا گردد.
برای اطلاع بیشتر می‌توانید به این مقاله مراجعه نمایید.

9.استفاده از MiniProfiler
سعی کنید از MiniProfiler در تمامی پروژه‌ها استفاده کنید. البته وقتی نرم افزار را در اختیار مصرف کننده قرار می‌دهید، آن را غیر فعال کنید. می‌توانید از متغیرهای compiler برای مجزا کردن build‌های متفاوت در برنامه خود استفاده کنید:
#if DEBUG then
// فعال سازی MiniProfiler
#endif
ایده دیگری هم وجود دارد. شما می‌توانید MiniProfiler را برای کاربر Admin یا کاربر Debugger فعال و برای بقیه غیر فعال کنید. در باب MiniProfiler مسائل زیادی وجود دارد که چند نمونه از آن در همین سایت در این مقاله و این مقاله در دسترس است. البته می‌توانید از ابزارهای دیگری مانند Glimpse که در این زمینه وجود دارد نیز استفاده کنید. لب کلام این نکته استفاده از profiler برای نرم افزار خود می‌باشد.

10. Data Paging در بانک اطلاعاتی
هنگامیکه از کامپوننت‌های شرکت‌های دیگر (Third party) استفاده می‌کنید، اطمینان حاصل کنید که صفحه بندی اطلاعات در بانک اطلاعاتی انجام می‌شود. برای نمونه کاپوننت گرید شرکت Telerik چند نوع صفحه بندی را پشتیبانی می‌کند. صفحه بندی سمت کاربر (توسط JavaScript)، صفحه بندی سمت سرور توسط کامپوننت و صفحه بندی مجازی. صفحه بندی سمت کاربر یعنی تمامی اطلاعات از سرور به کاربر فرستاده شده و در سمت کاربر عمل صفحه بندی انجام می‌شود. این یعنی واکشی تمامی اطلاعات از بانک و در مورد نرم افزارهای پرکاربر با حجم اطلاعات زیاد یعنی فاجعه. صفحه بندی سمت سرور ASP.NET هم یعنی واکشی اطلاعات از سرور بانک به سرور برنامه و سپس صفحه بندی توسط برنامه. این موضوع هم ممکن است مشکلات زیادی را ایجاد نماید چون باید حداقل تمامی رکوردها از اولین رکورد تا آخرین رکورد صفحه جاری از بانک واکشی شود که این عمل علاوه بر ایجاد سربار شبکه، سربار IO در بانک اطلاعاتی و سربار cpu در سرور ASP.NET ایجاد می‌کند. استفاده از صفحه بندی مجازی، شما را قادر می‌کند بتوانیم اطلاعات را در بانک صفحه بندی کرده و فقط صفحه مورد نظر خود را از بانک واکشی کنیم.
این حالت مجازی در اکثر component‌ها که توسط شرکت‌های مختلف ایجاد شده وجود دارد ولی ممکن است نام‌های متفاوتی داشته باشد. برای این موضوع باید به راهنمای component خریداری شده مراجعه کنید و یا به فروم‌ها و... مراجعه نمایید.

11. بررسی تعداد کوئری‌های صادر شده در یک صفحه و تعداد رکوردهای بازگشت داده شده توسط آن‌ها
این به این معنا نیست که برای هر query یک context مجزا ایجاد کنید، منظور این است که به بهانه اینکه اطلاعات مختلفی از جداول مختلف مورد نیاز است، query خود را آن قدر پیچیده یا گسترده ننویسیم که یا process آن در بانک زمان و سربار زیادی ایجاد کند و یا حجم اطلاعات بلا استفاده ای را از بانک به سرور برنامه لود نماید. به جای این موضوع می‌توانید در یک یا چند context دستورات مجزای واکشی اطلاعات صادر کنید تا تنها اطلاعات مورد نیاز خود را واکشی نمایید. البته این موضوع باعث نشود که تعداد query‌ها مثلا به 1000 عدد برسد! یعنی باید فیمابین query‌های پیچیده و query‌های ساده ولی با تعداد یکی را که مناسبتر با پروژه است انتخاب کنید که این موضوع با تجربه و تست حاصل می‌شود.
نظرات اشتراک‌ها
نحوه فارسی سازی فیلتر گرید کندو Kendo grid filter
با احترام
سوالی که بنده درگیر اون شدم اینه که در پروژه فیلدی هست که تاریخ رو به صورت میلادی ذخیره می‌کنه . با مواردی که گفته شده نمایش تاریخ به صورت شمسی انجام شده اما وقتی که فیلتر روی این فیلد فعال می‌شه در گرید کندو در قسمت باکس فیلتر یک کامپوننت DatePicker میلادی ظاهر می‌شه .
متاسفانه تمامی راه هایی که می‌دونستم رو تست کردم اما به نتیجه خاصی نرسیدم .
1. تلاش شد که UI فیلتر به صورت رشته تعریف بشه توسط دستورهای Format. و UI. اما موارد ارائه شده برای این قسمت کارساز نبود .
2. تلاش شد که با استفاده از یک property جانبی از نوع رشته و تبدیل فیلد تاریخ در متدهای set و get  این مسئله از راه دیگه حل بشه که نمایش اطلاعات موفقیت امیز بود اما در هنگام فیلتر متاسفانه به مشکلات تبدیل برخورد کردیم که قابل اصلاح نبود .
 لطف بفرمایید راهنمایی کنید برای فیلتر تاریخ چه راهکاری وجود داره

همچنین اگر دقت کنید مثالی هم که در این صفحه هست DatePicker فیلتر ، تاریخ رو به صورت 7 اسفند 2016 نشون می‌ده . 
مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت دهم - MobX Hooks و اعمال Async در Mobx
روشی را که تا اینجا در مورد MobX بررسی کردیم، تا نگارش 5x آن‌را پوشش می‌دهد. در همین زمان، کتابخانه‌ی دیگری به نام mobx-react-lite ارائه شد که به همراه تعدادی Hook مخصوص MobX بود تا با سیستم جدید React که مبتنی بر Hooks است، سازگار شود. این امکانات در حال حاضر با خود کتابخانه‌ی mobx-react 6x یکپارچه شده و به زودی mobx-react-lite منسوخ شده اعلام می‌شود. البته روش inject/observer بررسی شده‌ی تا نگارش 5x آن، هنوز هم برقرار است و قرار نیست که به این زودی‌ها منسوخ شده اعلام شود. به همین جهت نکاتی را که در مطلب جاری بررسی می‌کنیم، به عنوان روش تکمیلی سازگار با نگارش جاری 6x آن مطرح است و در کل با هر روشی که علاقمند بودید می‌توانید با MobX کار کنید. البته باز هم توصیه شده‌است که سیستم Provider آن‌را با React Context استاندارد، جایگزین کنید؛ چون احتمال حذف آن در نگارش‌های بعدی MobX هست.

به صورت خلاصه:
- اگر فقط از کامپوننت‌های کلاسی استفاده می‌کنید، mobx-react@5 برای کار شما پاسخگو است.
- اگر از کامپوننت‌های کلاسی و همچنین کامپوننت‌های تابعی در برنامه‌ی خود استفاده می‌کنید، mobx-react@6 به همراه mobx-react-lite نیز ارائه می‌شود و هر دو روش را با هم پوشش می‌دهد.
- اگر فقط از کامپوننت‌های تابعی جدید استفاده می‌کنید، هوک‌های کتابخانه‌ی کوچک mobx-react-lite برای کار شما کافی است.


معرفی useLocalStore Hook و useObserver Hook

در مطالب قبلی، روش تعریف یک کلاس مخزن حالت MobX را توسط تزئین کننده‌هایی مانند observable، computed و action بررسی کردیم. همچنین دریافتیم که تعریف یک چنین تزئین کننده‌هایی، یا نیاز به استفاده‌ی از تایپ‌اسکریپت را دارد و یا باید پروژه‌ی React را جهت تغییر کامپایلر Babel آن و فعالسازی decorators، مقداری ویرایش کرد. با useLocalStore Hook هرچند تمام روش‌های قبلی هنوز هم پشتیبانی می‌شوند، اما دیگر نیاز به استفاده‌ی از decorators نیست. useLocalStore تابعی است که یک شیء را باز می‌گرداند. هر خاصیتی از این شیء، به صورت خودکار observable درنظر گرفته می‌شود. تمام getters آن به عنوان computed properties تفسیر می‌شوند و تمام متدهای آن، action درنظر گرفته خواهند شد.
یک مثال:
import React from 'react'
import { useLocalStore, useObserver } from 'mobx-react' // 6.x

export const SmartTodo = () => {
  const todo = useLocalStore(() => ({
    title: 'Click to toggle',
    done: false,
    toggle() {
      todo.done = !todo.done
    },
    get emoji() {
      return todo.done ? '😜' : '🏃'
    },
  }))

  return useObserver(() => (
    <h3 onClick={todo.toggle}>
      {todo.title} {todo.emoji}
    </h3>
  ))
}
- در اینجا نحوه‌ی import تابع useLocalStore را از کتابخانه‌ی mobx-react نگارش 6x ملاحظه می‌کنید.
- روش استفاده‌ی از تابع useLocalStore، می‌تواند به صورت محلی (همانند اسم آن) مختص به یک کامپوننت باشد. یعنی می‌توان بجای state استاندارد React که اجازه‌ی تغییر مستقیم خواص آن‌را نمی‌دهد، از MobX State محلی ارائه شده‌ی توسط useLocalStore استفاده کرد و یا می‌توان useLocalStore را به صورت global نیز تعریف کرد که در ادامه‌ی بحث به آن می‌پردازیم.
- در مثال فوق، طول عمر شیء ایجاد شده‌ی توسط useLocalStore، محلی و محدود به طول عمر کامپوننت تابعی تعریف شده‌است.
- در اینجا شیء بازگشت داده شده‌ی توسط useLocalStore، دارای دو خاصیت title و done است. این دو خاصیت بدون نیاز به هیچ تعریف خاصی، observable در نظر گرفته می‌شوند. Fi به علاوه خاصیت getter آن به نام emoji نیز به عنوان یک خاصیت محاسباتی MobX تفسیر شده و متد toggle آن به صورت یک action پردازش می‌شود. بنابراین در حین کار با MobX Hooks دیگر نیازی به تغییر ساختار پروژه‌ی React، برای پشتیبانی از decorators نیست.
- در این مثال، return useObserver را نیز مشاهده می‌کنید. کار آن رندر مجدد کامپوننت، با تغییر یکی از خواص observable ردیابی شده‌ی توسط آن است.


امکان تعریف global state با کمک useLocalStore

نام useLocalStore از این جهت انتخاب شده‌است که مشخص کند مخزن حالت ایجاد شده‌ی توسط آن، درون یک کامپوننت به صورت محلی ایجاد می‌شود و سراسری نیست. اما این نکته به این معنا نیست که نمی‌توان مخزن حالت ایجاد شده‌ی توسط آن‌را در بین سلسه مراتب کامپوننت‌های برنامه به اشتراک گذاشت. توسط تابع useLocalStore می‌توان چندین مخزن حالت را ایجاد کرد و سپس توسط شیءای دیگر آن‌ها را یکی کرده و در آخر به کمک Context API خود React آن‌را در اختیار تمام کامپوننت‌های برنامه قرار داد.

تا نگارش MobX 5x (و همچنین پس از آن)، توسط inject@ می‌توان یک مخزن حالت را در اختیار یک کامپوننت قرار داد (مانند inject('myStore')). طراحی inject@ مربوط است به زمانیکه امکان دسترسی به Context پشت صحنه‌ی React به صورت عمومی توسط Context API آن ارائه نشده بود. به همین جهت از این پس دیگر نیازی به استفاده‌ی از آن نیست.


چگونه توسط MobX Hooks، یک مخزن حالت سراسری را ایجاد کنیم؟

برای ایجاد یک مخزن حالت سراسری با روش جدید MobX Hooks، مراحل زیر را می‌توان طی کرد:

الف) ایجاد شیء store
ابتدا متدی را مانند createStore ایجاد می‌کنیم، به نحوی که یک شیء را بازگشت دهد. این شیء همانطور که عنوان شد، خواصش، getters و متدهای آن، توسط MobX ردیابی خواهند شد (مانند const todo = useLocalStore مثال فوق) و نیازی به اعمال MobX Decorators را ندارند.
export function createStore() {
  return {
   // ...
  }
}

ب) برپایی Context
اینبار دیگر نه از شیء Provider خود MobX استفاده می‌کنیم و نه از تزئین کننده‌ی inject@ آن؛ بلکه از React Context استاندارد استفاده خواهیم کرد:
import React from 'react';
import { createStore } from './createStore';
import { useLocalStore } from 'mobx-react'; // 6.x or mobx-react-lite@1.4.0

const storeContext = React.createContext(null);

export const StoreProvider = ({ children }) => {
  const store = useLocalStore(createStore);
  return <storeContext.Provider value={store}>{children}</storeContext.Provider>;
}

export const useStore = () => {
  const store = React.useContext(storeContext);
  if (!store) {
    throw new Error('useStore must be used within a StoreProvider.');
  }
  return store
}
- در اینجا فرض شده‌است که تابع createStore که شیء store ما را ارائه می‌دهد از ماژولی به نام createStore دریافت می‌شود.
- سپس توسط React.createContext، یک شیء Context استاندارد React را ایجاد می‌کنیم؛ به نام storeContext.
- تابع کمکی StoreProvider، جایگزین شیء Provider قبلی MobX می‌شود. یعنی کارش محصور کردن کامپوننت App برنامه است تا شیء store را در اختیار سلسه مراتب کامپوننت‌های React قرار دهد. در اینجا children به همان کامپوننت‌هایی که قرار است توسط Context.Provider محصور شوند اشاره می‌کند.
- تابع کمکی useStore، جهت محصور کردن  متد React.useContext، اضافه شده‌است. می‌توانید useContext Hook را به صورت مستقیم در کامپوننت‌های تابعی فراخوانی کنید و یا می‌توانید از متد کمکی useStore بجای آن استفاده نمائید تا حجم کدهای تکراری برنامه کاهش یابد.

ج) استفاده‌ی از StoreProvider تهیه شده
اکنون با استفاده از متد StoreProvider فوق که شیء Context.Provider استاندارد React را بازگشت می‌دهد، می‌توان کامپوننت‌های بالاترین کامپوننت سلسه مراتب کامپوننت‌های برنامه را محصور کرد، تا تمام آن‌ها بتوانند به store ذخیره شده‌ی در Provider، دسترسی پیدا کنند:
export default function App() {
  return (
    <StoreProvider>
      <main>
        <Component1 />
        <Component2 />
        <Component3 />
      </main>
    </StoreProvider>
  );
}

د) استفاده از store مهیا شده در کامپوننت‌های تابعی برنامه
پس از تهیه‌ی متدی کمکی useStore که در حقیقت همان useContext Hook است، می‌توان به کمک آن در کامپوننت‌های تابعی، به store و تمام امکانات آن دسترسی پیدا کرد:
const store = useStore();
به این ترتیب دیگر نیازی به inject@ نخواهد بود.

سؤال: آیا هنوز هم می‌توان یک مخزن پیچیده‌ی متشکل از چندین کلاس را تشکیل داد؟
پاسخ: بله. برای مثال ابتدا دو کلاس جدید CounterStore و ThemeStore را به نحو متداولی، با استفاده‌ی از MobX decorators طراحی می‌کنیم (دقیقا مانند مثال قسمت قبل). سپس بجای ذکر نال، بجای پارامتر متد createContext، آن‌را با یک شیء جدید مقدار دهی می‌کنیم که هر کدام از خواص آن، به یک وهله از مخازن حالت ایجاد شده اشاره می‌کند:
export const storesContext = React.createContext({
  counterStore: new CounterStore(),
  themeStore: new ThemeStore(),
});

export const useStores = () => React.useContext(storesContext);
با این تعییر اگر در کامپوننتی از برنامه نیاز به برای مثال شیء منتسب به خاصیت counterStore را داشتیم، می‌توان به صورت زیر عمل کرد:
const { counterStore } = useStores();


چند نکته‌ی تکمیلی

نکته 1: با اشیاء MobX از Object Destructuring استفاده نکنید!

اگر بر روی اشیاء MobX از Object Destructuring استفاده کنیم، خروجی آن تبدیل به متغیرهای ساده‌ای خواهند شد که دیگر ردیابی نمی‌شوند.
برای مثال اگر counterStore مثال فوق به همراه خاصیت observable ای به نام activeUserName است، آن‌را به صورت زیر تبدیل به متغیر activeUserName نکنید؛ چون دیگر reactive نخواهد بود:
const {
    counterStore: { activeUserName },
} = useStores();
فقط بالاترین سطح مخزن را به صورت زیر توسط Object Destructuring از آن استخراج و سپس استفاده کنید:
const { counterStore } = useStores();


نکته 2: مدیریت side effects با MobX

در مورد اثرات جانبی و side effects در مطلب «قسمت 32 - React Hooks - بخش 3 - نکات ویژه‌ی برقراری ارتباط با سرور» بیشتر بحث شد. اگر یک اثر جانبی مانند تنظیم document.title، به مقدار یک خاصیت observable وابسته بود، می‌توان از متد autorun که تغییرات آن‌ها را ردیابی می‌کند، درون useEffect Hook استاندارد، استفاده کرد:
import React from 'react'
import { autorun } from 'mobx'

function useDocumentTitle(store) {
  React.useEffect(
    () =>
      autorun(() => {
        document.title = `${store.title} - ${store.sectionName}`
      }),
    [], // note empty dependencies
  )
}
در حین کار با MobX، هیچگاه نیازی به ذکر وابستگی‌های تابع useEffect نیست؛ چون اساسا وجود خارجی ندارند و توسط خود MobX مدیریت می‌شوند و به store وابسته‌اند و نه به حالت کامپوننت جاری.


نکته 4: روش فعالسازی MobX strict mode

اگر strict mode را در Mobx به روش زیر فعال کنیم:
import { configure } from "mobx";
configure({ enforceActions: true });
پس از آن باید حالت مدیریت شده‌ی توسط MobX را فقط و فقط توسط action‌های آن تغییر داد و اگر سعی در تغییر مقدار مستقیم یک خاصیت observable کنیم، استثنایی صادر خواهد شد. برای تغییر خواص observable باید آن‌ها را درون یک action قرار داد؛ تا مطابق رهنمودهای طراحی کلاس‌های MobX باشد.


نکته 3: روش انجام اعمال async در MobX

فرض کنید یک عملیات async را در یک اکشن متد کلاس حالت MobX، به صورت زیر انجام داده‌ایم و نتیجه‌ی آن به خاصیت weatherData آن کلاس که observable است، به صورت مستقیم انتساب داده شده‌است:
@action
loadWeather = city => {
  fetch(
    `https://abnormal-weather-api.herokuapp.com/cities/search?city=${city}`
  )
    .then(response => response.json())
    .then(data => {
      this.weatherData = data;
    });
};
هرچند loadWeather یک متد را ارائه می‌دهد که به صورت action معرفی شده‌است، اما هرچیزی که داخل آن قرار می‌گیرد، الزاما تحت کنترل آن نیست. برای مثال متد then، یک تابع callback جدید را فراخوانی می‌کند که اعمال آن، تحت کنترل loadWeather نیست. به همین جهت اگر strict mode را فعال کرده باشیم، عنوان می‌کند که خواص observable را باید درون یک اکشن متد تغییر داد و نه به صورت مستقیم؛ مانند this.weatherData در اینجا.

راه حل اول: تغییر خاصیت this.weatherData را به یک اکشن متد مجزا انتقال می‌دهیم:
@action setWeather = data => {
    this.weatherData = data;
};
اکنون می‌توان قسمت then را به صورت then(data => this.setWeather(data)) نوشت و خطای یاد شده برطرف می‌شود.

راه حل دوم: اگر نمی‌خواهیم یک اکشن متد جدید را تعریف کنیم، می‌توان از متد کمکی runInAction در داخل یک callback استفاده کرد:
  loadWeatherInline = city => {
    fetch(`http://jsonplaceholder.typicode.com/comments/${city}`)
      .then(response => response.json())
      .then(data => {
        runInAction(() => (this.weatherData = data));
      });
  };
runInAction یکی از متدهای قابل دریافت از mobx است.

در مورد اعمال async/await چطور؟
در اینجا هم تفاوتی نمی‌کند. هر چیزی پس از await، شبیه به حالت متد then پردازش می‌شود. به همین جهت در اینجا نیز باید از یکی از دو راه حل ارائه شده، استفاده کرد:
  loadWeatherAsync = async city => {
    const response = await fetch(
      `http://jsonplaceholder.typicode.com/comments/${city}`
    );
    const data = await response.json();
    runInAction(() => {
      this.weatherData = data;
    });
  };