طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سیزده دقیقه

در طی چند قسمت، نحوه‌ی طراحی یک سیستم افزونه پذیر را با ASP.NET MVC بررسی خواهیم کرد. عناوین مواردی که در این سری پیاده سازی خواهند شد به ترتیب ذیل هستند:
1- چگونه Area‌های استاندارد را تبدیل به یک افزونه‌ی مجزا و منتقل شده‌ی به یک اسمبلی دیگر کنیم.
2- چگونه ساختار پایه‌ای را جهت تامین نیازهای هر افزونه جهت تزریق وابستگی‌ها تا ثبت مسیریابی‌ها و امثال آن تدارک ببینیم.
3- چگونه فایل‌های CSS ، JS و همچنین تصاویر ثابت هر افزونه را داخل اسمبلی آن قرار دهیم تا دیگر نیازی به ارائه‌ی مجزای آ‌ن‌ها نباشد.
4- چگونه Entity Framework Code-First را با این طراحی یکپارچه کرده و از آن جهت یافتن خودکار مدل‌ها و موجودیت‌های خاص هر افزونه استفاده کنیم؛ به همراه مباحث Migrations خودکار و همچنین پیاده سازی الگوی واحد کار.


در مطلب جاری، موارد اول و دوم بررسی خواهند شد. پیشنیازهای آن مطالب ذیل هستند:
الف) منظور از یک Area چیست؟
ب) توزیع پروژه‌های ASP.NET MVC بدون ارائه فایل‌های View آن
ج) آشنایی با تزریق وابستگی‌ها در ASP.NET MVC و همچنین اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
د) آشنایی با رخدادهای Build


تبدیل یک Area به یک افزونه‌ی مستقل

روش‌های زیادی برای خارج کردن Areaهای استاندارد ASP.NET MVC از یک پروژه و قرار دادن آن‌ها در اسمبلی‌های دیگر وجود دارند؛ اما در حال حاضر تنها روشی که نگهداری می‌شود و همچنین اعضای آن همان اعضای تیم نیوگت و ASP.NET MVC هستند، همان روش استفاده از Razor Generator است.
بنابراین ساختار ابتدایی پروژه‌ی افزونه پذیر ما به صورت ذیل خواهد بود:
1) ابتدا افزونه‌ی Razor Generator را نصب کنید.
2) سپس یک پروژه‌ی معمولی ASP.NET MVC را آغاز کنید. در این سری نام MvcPluginMasterApp برای آن در نظر گرفته شده‌است.
3) در ادامه یک پروژه‌ی معمولی دیگر ASP.NET MVC را نیز به پروژه‌ی جاری اضافه کنید. برای مثال نام آن در اینجا MvcPluginMasterApp.Plugin1 تنظیم شده‌است.
4) به پروژه‌ی MvcPluginMasterApp.Plugin1 یک Area جدید و معمولی را به نام NewsArea اضافه کنید.
5) از پروژه‌ی افزونه، تمام پوشه‌های غیر Area را حذف کنید. پوشه‌های Controllers و Models و Views حذف خواهند شد. همچنین فایل global.asax آن‌را نیز حذف کنید. هر افزونه، کنترلرها و Viewهای خود را از طریق Area مرتبط دریافت می‌کند و در این حالت دیگر نیازی به پوشه‌های Controllers و Models و Views واقع شده در ریشه‌ی اصلی پروژه‌ی افزونه نیست.
6) در ادامه کنسول پاور شل نیوگت را باز کرده و دستور ذیل را صادر کنید:
  PM> Install-Package RazorGenerator.Mvc
این دستور را باید یکبار بر روی پروژه‌ی اصلی و یکبار بر روی پروژه‌ی افزونه، اجرا کنید.


همانطور که در تصویر نیز مشخص شده‌است، برای اجرای دستور نصب RazorGenerator.Mvc نیاز است هربار پروژه‌ی پیش فرض را تغییر دهید.
7) اکنون پس از نصب RazorGenerator.Mvc، نوبت به اجرای آن بر روی هر دو پروژه‌ی اصلی و افزونه است:
  PM> Enable-RazorGenerator
بدیهی است این دستور را نیز باید همانند تصویر فوق، یکبار بر روی پروژه‌ی اصلی و یکبار بر روی پروژه‌ی افزونه اجرا کنید.
همچنین هربار که View جدیدی اضافه می‌شود نیز باید این‌کار را تکرار کنید یا اینکه مطابق شکل زیر، به خواص View جدید مراجعه کرده و Custom tool آن‌را به صورت دستی به RazorGenerator تنظیم نمائید. دستور Enable-RazorGenerator این‌کار را به صورت خودکار انجام می‌دهد.


تا اینجا موفق شدیم Viewهای افزونه را داخل فایل dll آن مدفون کنیم. به این ترتیب با کپی کردن افزونه به پوشه‌ی bin پروژه‌ی اصلی، دیگر نیازی به ارائه‌ی فایل‌های View آن نیست و تمام اطلاعات کنترلرها، مدل‌ها و Viewها به صورت یکجا از فایل dll افزونه‌ی ارائه شده خوانده می‌شوند.


کپی کردن خودکار افزونه به پوشه‌ی Bin پروژه‌ی اصلی

پس از اینکه ساختار اصلی کار شکل گرفت، هربار پس از کامپایل افزونه (یا افزونه‌ها)، نیاز است فایل‌های پوشه‌ی bin آن‌را به پوشه‌ی bin پروژه‌ی اصلی کپی کنیم (پروژه‌ی اصلی در این حالت هیچ ارجاع مستقیمی را به افزونه‌ی جدید نخواهد داشت). برای خودکار سازی این کار، به خواص پروژه‌ی افزونه مراجعه کرده و قسمت Build events آن‌را به نحو ذیل تنظیم کنید:


در اینجا دستور ذیل در قسمت Post-build event نوشته شده است:
 Copy "$(ProjectDir)$(OutDir)$(TargetName).*" "$(SolutionDir)MvcPluginMasterApp\bin\"
و سبب خواهد شد تا پس از هر کامپایل موفق، فایل‌های اسمبلی افزونه به پوشه‌ی bin پروژه‌ی MvcPluginMasterApp به صورت خودکار کپی شوند.


تنظیم فضاهای نام کلیه مسیریابی‌های پروژه

در همین حالت اگر پروژه را اجرا کنید، موتور ASP.NET MVC به صورت خودکار اطلاعات افزونه‌ی کپی شده به پوشه‌ی bin را دریافت و به Application domain جاری اعمال می‌کند؛ برای اینکار نیازی به کد نویسی اضافه‌تری نیست و خودکار است. برای آزمایش آن فقط کافی است یک break point را داخل کلاس RazorGeneratorMvcStart افزونه قرار دهید.
اما ... پس از اجرا، بلافاصله پیام تداخل فضاهای نام را دریافت می‌کنید. خطاهای حاصل عنوان می‌کند که در App domain جاری، دو کنترلر Home وجود دارند؛ یکی در پروژه‌ی اصلی و دیگری در پروژه‌ی افزونه و مشخص نیست که مسیریابی‌ها باید به کدامیک ختم شوند.
برای رفع این مشکل، به فایل NewsAreaAreaRegistration.cs پروژه‌ی افزونه مراجعه کرده و مسیریابی آن‌را به نحو ذیل تکمیل کنید تا فضای نام اختصاصی این Area صریحا مشخص گردد.
using System.Web.Mvc;
 
namespace MvcPluginMasterApp.Plugin1.Areas.NewsArea
{
    public class NewsAreaAreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get
            {
                return "NewsArea";
            }
        }
 
        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "NewsArea_default",
                "NewsArea/{controller}/{action}/{id}",
                // تکمیل نام کنترلر پیش فرض
                new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمت‌های برنامه
                namespaces: new[] { string.Format("{0}.Controllers", this.GetType().Namespace) }
            );
        }
    }
}
همینکار را باید در پروژه‌ی اصلی و هر پروژه‌ی افزونه‌ی جدیدی نیز تکرار کرد. برای مثال به فایل RouteConfig.cs پروژه‌ی اصلی مراجعه کرده و تنظیم ذیل را اعمال نمائید:
using System.Web.Mvc;
using System.Web.Routing;
 
namespace MvcPluginMasterApp
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمت‌های برنامه
                namespaces: new[] { string.Format("{0}.Controllers", typeof(RouteConfig).Namespace) }
            );
        }
    }
}
بدون تنظیم فضاهای نام هر مسیریابی، امکان استفاده‌ی بهینه و بدون خطا از Areaها وجود نخواهد داشت.


طراحی قرارداد پایه افزونه‌ها

تا اینجا با نحوه‌ی تشکیل ساختار هر پروژه‌ی افزونه آشنا شدیم. اما هر افزونه در آینده نیاز به مواردی مانند منوی اختصاصی در منوی اصلی سایت، تنظیمات مسیریابی اختصاصی، تنظیمات EF و  امثال آن نیز خواهد داشت. به همین منظور، یک پروژه‌ی class library جدید را به نام MvcPluginMasterApp.PluginsBase آغاز کنید.
سپس قرار داد IPlugin را به نحو ذیل به آن اضافه نمائید:
using System;
using System.Reflection;
using System.Web.Optimization;
using System.Web.Routing;
using StructureMap;
 
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IPlugin
    {
        EfBootstrapper GetEfBootstrapper();
        MenuItem GetMenuItem(RequestContext requestContext);
        void RegisterBundles(BundleCollection bundles);
        void RegisterRoutes(RouteCollection routes);
        void RegisterServices(IContainer container);
    }
 
    public class EfBootstrapper
    {
        /// <summary>
        /// Assemblies containing EntityTypeConfiguration classes.
        /// </summary>
        public Assembly[] ConfigurationsAssemblies { get; set; }
 
        /// <summary>
        /// Domain classes.
        /// </summary>
        public Type[] DomainEntities { get; set; }
 
        /// <summary>
        /// Custom Seed method.
        /// </summary>
        //public Action<IUnitOfWork> DatabaseSeeder { get; set; }
    }
 
    public class MenuItem
    {
        public string Name { set; get; }
        public string Url { set; get; }
    }
}
پروژه‌ی این قرارداد برای کامپایل شدن، نیاز به بسته‌های نیوگت ذیل دارد:
PM> install-package EntityFramework
PM> install-package Microsoft.AspNet.Web.Optimization
PM> install-package structuremap.web
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.


توضیحات قرار داد IPlugin

از این پس هر افزونه باید دارای کلاسی باشد که از اینترفیس IPlugin مشتق می‌شود. برای مثال فعلا کلاس ذیل را به افزونه‌ی پروژه اضافه نمائید:
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using MvcPluginMasterApp.PluginsBase;
using StructureMap;
 
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public EfBootstrapper GetEfBootstrapper()
        {
            return null;
        }
 
        public MenuItem GetMenuItem(RequestContext requestContext)
        {
            return new MenuItem
            {
                Name = "Plugin 1",
                Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" })
            };
        }
 
        public void RegisterBundles(BundleCollection bundles)
        {
            //todo: ...
        }
 
        public void RegisterRoutes(RouteCollection routes)
        {
            //todo: add custom routes.
        }
 
        public void RegisterServices(IContainer container)
        {
            // todo: add custom services.
 
            container.Configure(cfg =>
            {
                //cfg.For<INewsService>().Use<EfNewsService>();
            });
        }
    }
}
در قسمت جاری فقط از متد GetMenuItem آن استفاده خواهیم کرد. در قسمت‌های بعد، تنظیمات EF، تنظیمات مسیریابی‌ها و Bundling و همچنین ثبت سرویس‌های افزونه را نیز بررسی خواهیم کرد.
برای اینکه هر افزونه در منوی اصلی ظاهر شود، نیاز به یک نام، به همراه آدرسی به صفحه‌ی اصلی آن خواهد داشت. به همین جهت در متد GetMenuItem نحوه‌ی ساخت آدرسی را به اکشن متد Index کنترلر Home واقع در Area‌ایی به نام NewsArea، مشاهده می‌کنید.


بارگذاری و تشخیص خودکار افزونه‌ها

پس از اینکه هر افزونه دارای کلاسی مشتق شده از قرارداد IPlugin شد، نیاز است آن‌ها را به صورت خودکار یافته و سپس پردازش کنیم. این‌کار را به کتابخانه‌ی StructureMap واگذار خواهیم کرد. برای این منظور پروژه‌ی جدیدی را به نام MvcPluginMasterApp.IoCConfig آغاز کرده و سپس تنظیمات آن‌را به نحو ذیل تغییر دهید:
using System;
using System.IO;
using System.Threading;
using System.Web;
using MvcPluginMasterApp.PluginsBase;
using StructureMap;
using StructureMap.Graph;
 
namespace MvcPluginMasterApp.IoCConfig
{
    public static class SmObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }
 
        private static Container defaultContainer()
        {
            return new Container(cfg =>
            {
                cfg.Scan(scanner =>
                {
                    scanner.AssembliesFromPath(
                        path: Path.Combine(HttpRuntime.AppDomainAppPath, "bin"),
                            // یک اسمبلی نباید دوبار بارگذاری شود
                        assemblyFilter: assembly =>
                        {
                            return !assembly.FullName.Equals(typeof(IPlugin).Assembly.FullName);
                        });
 
                    scanner.WithDefaultConventions(); //Connects 'IName' interface to 'Name' class automatically.
                    scanner.AddAllTypesOf<IPlugin>().NameBy(item => item.FullName);
                });
            });
        }
    }
}
این پروژه‌ی class library جدید برای کامپایل شدن نیاز به بسته‌های نیوگت ذیل دارد:
PM> install-package EntityFramework
PM> install-package structuremap.web
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.

کاری که در کلاس SmObjectFactory انجام شده، بسیار ساده است. مسیر پوشه‌ی Bin پروژه‌ی اصلی به structuremap معرفی شده‌است. سپس به آن گفته‌ایم که تنها اسمبلی‌هایی را که دارای اینترفیس IPlugin هستند، به صورت خودکار بارگذاری کن. در ادامه تمام نوع‌های IPlugin را نیز به صورت خودکار یافته و در مخزن تنظیمات خود، اضافه کن.


تامین نیازهای مسیریابی و Bundling هر افزونه به صورت خودکار

در ادامه به پروژه‌ی اصلی مراجعه کرده و در پوشه‌ی App_Start آن کلاس ذیل را اضافه کنید:
using System.Linq;
using System.Web.Optimization;
using System.Web.Routing;
using MvcPluginMasterApp;
using MvcPluginMasterApp.IoCConfig;
using MvcPluginMasterApp.PluginsBase;
 
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(PluginsStart), "Start")]
 
namespace MvcPluginMasterApp
{
    public static class PluginsStart
    {
        public static void Start()
        {
            var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
            foreach (var plugin in plugins)
            {
                plugin.RegisterServices(SmObjectFactory.Container);
                plugin.RegisterRoutes(RouteTable.Routes);
                plugin.RegisterBundles(BundleTable.Bundles);
            }
        }
    }
}
بدیهی است در این حالت نیاز است ارجاعی را به پروژه‌ی MvcPluginMasterApp.PluginsBase به پروژه‌ی اصلی اضافه کنیم.
دراینجا با استفاده از کتابخانه‌ای به نام WebActivatorEx (که باز هم توسط نویسندگان اصلی Razor Generator تهیه شده‌است)، یک متد PostApplicationStartMethod سفارشی را تعریف کرده‌ایم.
مزیت استفاده از اینکار این است که فایل Global.asax.cs برنامه شلوغ نخواهد شد. در غیر اینصورت باید تمام این کدها را در انتهای متد Application_Start قرار می‌دادیم.
در اینجا با استفاده از structuremap، تمام افزونه‌های موجود به صورت خودکار بررسی شده و سپس پیشنیازهای مسیریابی و Bundling و همچنین تنظیمات IoC Container مورد نیاز آن‌ها به هر افزونه به صورت مستقل، تزریق خواهد شد.


اضافه کردن منو‌های خودکار افزونه‌ها به پروژه‌ی اصلی

پس از اینکه کار پردازش اولیه‌ی IPluginها به پایان رسید، اکنون نوبت به نمایش آدرس اختصاصی هر افزونه در منوی اصلی سایت است. برای این منظور فایل جدیدی را به نام PluginsMenu.cshtml_، در پوشه‌ی shared پروژه‌ی اصلی اضافه کنید؛ با این محتوا:
@using MvcPluginMasterApp.IoCConfig
@using MvcPluginMasterApp.PluginsBase
@{
    var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
}
 
@foreach (var plugin in plugins)
{
    var menuItem = plugin.GetMenuItem(this.Request.RequestContext);
    <li>
        <a href="@menuItem.Url">@menuItem.Name</a>
    </li>
}
در اینجا تمام افزونه‌ها به کمک structuremap یافت شده و سپس آیتم‌های منوی آن‌ها به صورت خودکار دریافت و اضافه می‌شوند.
سپس به فایل Layout.cshtml_ پروژه‌ی اصلی مراجعه و توسط فراخوانی Html.RenderPartial، آن‌را در بین سایر آیتم‌های منوی اصلی اضافه می‌کنیم:
<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            @Html.ActionLink("MvcPlugin Master App", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li>@Html.ActionLink("Master App/Home", "Index", "Home", new {area = ""}, null)</li>
                @{ Html.RenderPartial("_PluginsMenu"); }
            </ul>
        </div>
    </div>
</div>
اکنون اگر پروژه را اجرا کنیم، یک چنین شکلی را خواهد داشت:



بنابراین به صورت خلاصه

1) هر افزونه، یک پروژه‌ی کامل ASP.NET MVC است که پوشه‌های ریشه‌ی اصلی آن حذف شده‌اند و اطلاعات آن توسط یک Area جدید تامین می‌شوند.
2) تنظیم فضای نام مسیریابی‌های تمام پروژه‌ها را فراموش نکنید. در غیر اینصورت شاهد تداخل پردازش کنترلرهای هم نام خواهید بود.
3) جهت سهولت کار، می‌توان فایل‌های bin هر افزونه را توسط رخداد post-build، به پوشه‌ی bin پروژه‌ی اصلی کپی کرد.
4) Viewهای هر افزونه توسط Razor Generator در فایل dll آن مدفون خواهند شد.
5) هر افزونه باید دارای کلاسی باشد که اینترفیس IPlugin را پیاده سازی می‌کند. از این اینترفیس برای ثبت اطلاعات هر افزونه یا دریافت اطلاعات سفارشی از آن کمک می‌گیریم.
6) با استفاده از استراکچرمپ و قرارداد IPlugin، منوهای هر افزونه را به صورت خودکار یافته و سپس به فایل layout اصلی اضافه می‌کنیم.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
MvcPluginMasterApp-Part1.zip
  • #
    ‫۹ سال و ۵ ماه قبل، پنجشنبه ۲۷ فروردین ۱۳۹۴، ساعت ۲۰:۵۱
    یک سوال، هنگام حذف افزونه با توجه به اینکه ممکنه کاربری در حال کار با بخش‌های مختلف اون باشه چه اتفاقی برای حذف ارجاع‌های اون به برنامه می‌افتد؟ آیا اجازه حذف لازم است؟
    • #
      ‫۹ سال و ۵ ماه قبل، پنجشنبه ۲۷ فروردین ۱۳۹۴، ساعت ۲۰:۵۵
      - برنامه‌ی اصلی ارجاع مستقیمی را به هیچ افزونه‌ای ندارد.
      + هر نوع تغییری در پوشه‌ی bin برنامه سبب ری استارت آن خواهد شد. بنابراین اگر افزونه‌ای اضافه شود، برنامه به صورت خودکار ری استارت شده و بلافاصله افزونه‌ی جدید، قابل استفاده خواهد بود. اگر فایل افزونه‌ای از پوشه‌ی bin حذف شود، باز هم سبب ری استارت برنامه و بارگذاری خودکار منوها و محاسبه‌ی مجدد آن‌ها می‌گردد که اینبار دیگر شامل اطلاعات افزونه‌ی حذف شده نیست.
      • #
        ‫۹ سال و ۴ ماه قبل، سه‌شنبه ۲۲ اردیبهشت ۱۳۹۴، ساعت ۱۳:۱۲
        سلام
        - آیا این امکان هست که فایل‌های افزونه در پوشه bin برنامه اصلی نباشد و در پوشه دیگری در برنامه اصلی باشد(بعنوان مثال: Plugins) ؟
        - آیا مهم‌ترین هدف وجود فایل‌های افزونه‌ها در پوشه bin برنامه اصلی, ری استارت شدن برنامه اصلی می‌باشد که بلافاصله تغییرات(حذف و یا افزوده شدن یک یا چند افزونه) اعمال و مشاهده شود  ؟
        • #
          ‫۹ سال و ۴ ماه قبل، سه‌شنبه ۲۲ اردیبهشت ۱۳۹۴، ساعت ۱۴:۳۶
          نگارش فعلی WebActivatorEx، اسمبلی‌های خارج از پوشه‌ی bin را پردازش نمی‌کند. از آن برای مدیریت خودکار آغاز تعدادی راه انداز استفاده شده‌است. همچنین مسیریابی‌های Areaهای اضافه شده یا تنظیمات EF هم فقط در حین آغاز برنامه یکبار خوانده شده و سپس کش می‌شوند (برای بالا بردن سرعت کار).
  • #
    ‫۹ سال و ۵ ماه قبل، جمعه ۲۸ فروردین ۱۳۹۴، ساعت ۰۲:۱۱
    سلام؛ یه سوال امنیتی، آیا راهکاری دارید که کسی به طور غیر مجاز برای برنامه پلاگین ننویسه منظور این هستش که فردی که پلاگین رو نوشته فقط با تایید بتونه فعالش کنه و از لحاظ امنیتی قابل چک باشه و بدون تایید اجرایی نشه چون من نگران هستم فردی پلاگین بنویسد و عمدا یا غیر عمد پلاگینی توسعه دهد که اطلاعات و روند فعالیت برنامه را جاسوسی کند
    خودم این ذهنیت رو دارم که هش کد هر پلاگین باید توسط مدیر تایید بشه و سپس قابل اجرا باشه تا کسی نتونه بعدا پلاگین را تغییر بده و امنیت سیستم را به خطر بنداره
    در کل ملاحظات امنیتی پاگین‌ها را چگونه در نظر بگیریم ؟
    • #
      ‫۹ سال و ۵ ماه قبل، جمعه ۲۸ فروردین ۱۳۹۴، ساعت ۰۲:۳۵
      از مطلب «تهیه XML امضاء شده جهت تولید مجوز استفاده از برنامه» ایده بگیرید. یک متد GetLicense به اینترفیس IPlugin اضافه کنید و در آن مجوز ارائه شده توسط افزونه را در برنامه‌ی اصلی بررسی کنید (در کلاس PluginsStart و همچنین فایل PluginsMenu.cshtml_). فقط کسانی می‌توانند «XML امضاء شده» تولید کنند که دسترسی به کلیدهای خصوصی و امن شما را داشته باشند.  
  • #
    ‫۹ سال و ۵ ماه قبل، یکشنبه ۳۰ فروردین ۱۳۹۴، ساعت ۲۰:۲۳
    سلام؛ اگر بخواهیم مسیر یابی پروژه را به attribute routing تغییر بدهیم چه کارهایی باید انجام دهیم.
  • #
    ‫۹ سال و ۵ ماه قبل، شنبه ۵ اردیبهشت ۱۳۹۴، ساعت ۱۸:۱۲
    با تشکر به خاطر مطلب مفیدی که منتشر کردید.
    مشکلی که به آن برخوردم این است که افزونه به خوبی در پروژه اصلی بار گذاری میشود ولی متد RegisterArea مربوط به Area موجود در افزونه اجرا نمیشود .
    • #
      ‫۹ سال و ۵ ماه قبل، شنبه ۵ اردیبهشت ۱۳۹۴، ساعت ۱۸:۵۵
      «... به خوبی در پروژه اصلی بار گذاری میشود ...»
      یعنی منوی پویای افزونه‌ی مرتبط در پروژه‌ی اصلی کار می‌کند و اضافه می‌شود و همچنین با کلیک بر روی آن، صفحه‌ی اصلی افزونه ظاهر می‌شود؟ اگر بله، یعنی مشکلی در یافتن آن نبوده‌است و مسیریابی آن اضافه شده‌است. اگر مسیریابی آن خوانده نشود، با کلیک بر روی منوی پویای آن، صفحه‌ی اصلی افزونه ظاهر نمی‌شود.
      در کل بررسی کنید:
      - آیا پروژه‌ی افزونه‌ای که ایجاد کردید از نوع ASP.NET MVC است یا خیر؟
      - آیا فایل‌های پوشه‌ی bin آن در پوشه‌ی bin پروژه‌ی اصلی کپی شده‌اند یا خیر؟
      - اگر این افزونه یک سری وابستگی اضافه‌تر دارد که در پروژه‌ی اصلی ارجاعی ندارند، این فایل‌ها هم باید در پوشه‌ی bin پروژه‌ی اصلی کپی شوند وگرنه این افزونه بارگذاری نخواهد شد.

      دو مثال افزونه به همراه کدهای این پروژه  هست، سورس خودتان را با آن انطباق دهید.
      • #
        ‫۹ سال و ۵ ماه قبل، شنبه ۵ اردیبهشت ۱۳۹۴، ساعت ۱۹:۰۹
        بله پروژه از نوع Asp.net MVC است. بنده افزونه را در فولدر Plugins ایجاد کردم و سپس یک فولدر در داخل فولدر Plugins به نام Blog ساختم و پروژه‌های افزونه را به داخل آن انتقال دادم (مشکل این موقع به وجود آمد و دلیل آن را نمیدانم) ! با برگرداندن پروژه‌ها به فولدر قبلی، متد RegisterArea هم کار کرد. ولی با این که من namespaces مربوط به Routing پروژه‌ها را ست کردم ولی با این حال با کلیک بر روی منوی مربوط به افزونه ردایرکت میشود به صفحه اصلی پروژه.
        این کانفیگ مربوط به افزونه
         public override void RegisterArea(AreaRegistrationContext context)
                {
                    context.MapRoute(
                        "BlogArea_default",
                        "BlogArea/{controller}/{action}/{id}",
                        // تکمیل نام کنترلر پیش فرض
                        new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                        // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمت‌های برنامه
                        namespaces: new[] { string.Format("{0}.Controllers", this.GetType().Namespace) }
                    );
                }
        و این هم لینک تولیدی برای افزونه 
        Url = new UrlHelper(requestContext).Action("Index", "Home",new{area="BlogArea"})

        کانفیگ مربوط به پروژه اصلی 
         routes.MapRoute(
                        name: "Default",
                        url: "{controller}/{action}/{id}",
                        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                        namespaces: new[] { string.Format("{0}.Controllers", typeof(RouteConfig).Namespace) }
                    );
        • #
          ‫۹ سال و ۵ ماه قبل، شنبه ۵ اردیبهشت ۱۳۹۴، ساعت ۲۰:۵۵
          - جهت آزمایش بیشتر، دو پوشه برای افزونه‌ها ایجاد و تمام فایل‌های آن‌ها منتقل شدند. مشکلی مشاهده نشد.
          - اگر فضاهای نام را تغییر دادید، بهتر است از منوی Build یکبار گزینه‌ی Clean solution را اجرا کنید تا فایل‌های قدیمی حذف شوند و تداخل ایجاد نکنند. سپس پروژه را مجددا Build کنید.
          • #
            ‫۹ سال و ۵ ماه قبل، سه‌شنبه ۸ اردیبهشت ۱۳۹۴، ساعت ۱۵:۴۴
            مشکل حل نشد.در واقع مشکل فقط مربوط است به سیستم مسیریابی با وجود اینکه تمام تنظیمات رو انجام دادم تا تداخلی به وجود نیاید .
            • #
              ‫۹ سال و ۵ ماه قبل، سه‌شنبه ۸ اردیبهشت ۱۳۹۴، ساعت ۱۹:۵۷
              - زمانیکه پوشه‌های پروژه‌ها را جابجا می‌کنید، باید تمام فایل‌های csproj آن‌ها را باز کنید و سپس مسیرهای HintPath بسته‌های نیوگت را اصلاح کنید:
               <HintPath>..\..\..\packages\T4MVCExtensions.3.15.0\lib\net40\T4MVCExtensions.dll</HintPath>
              اگر اینکار رخ ندهد، عملا کار بازیابی بسته‌ها پاسخ نخواهد داد چون HintPath‌های موجود به چند سطح بالاتر اشاره نمی‌کنند:
               <HintPath>..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll</HintPath>
              - در پروژه‌ی RabbalShopCMS.DomainClasses شما به نظر یک سری کلاس‌ها نیستند و اضافه نشدند به سورس کنترل.
              - قسمت post build event باید به صورت ذیل اصلاح شود:
               Copy "$(ProjectDir)$(OutDir)*.*" "$(SolutionDir)RabbalShopCMS.Web\bin\"
              به این صورت تمام فایل‌های مرتبط کپی می‌شوند.
              - در global.asax.cs پروژه‌ی اصلی باید این موارد را حذف کنید:
               ViewEngines.Engines.Clear();
              ViewEngines.Engines.Add(new RazorViewEngine ());
              Razor generator به ازای هر پلاگین دارای یک فایل RazorGeneratorMvcStart است که کارش ثبت یک ViewEngine مخصوص خواندن فایل‌های View از اسمبلی برنامه است که این موارد نباید حذف شوند و اگر حذف شوند، Viewهای پلاگین‌ها قابل مشاهده نخواهند بود.
              - افزونه‌ی دارای Area نیازی نیست فایل layout داشته باشد. فقط باید دارای یک ViewStart باشد که به layout پروژه‌ی اصلی اشاره کند. این layout از پروژه‌ی پایه دریافت می‌شود و نه از افزونه. بنابراین فایل layout افزونه باید حذف شود و اضافی است.
              - بعد در حالت solution چند پروژه‌ای اجرای دستور ذیل الزامی است: (خیلی مهم)
               PM> update-package
              این مورد سبب خواهد شد تا تمام وابستگی‌های solution جاری به همراه تمام پروژه‌های مرتبط آن یکدست شوند.
              - اگر با درخواست یک آدرس، فایل view پروژه‌ی دیگری بازگشت داده شد، ترتیب اضافه شدن PrecompiledMvcEngine را تغییر دهید. برای مثال در پروژه‌ی پلاگین:
               ViewEngines.Engines.Insert(0, engine);
              در پروژه‌ی اصلی:
               ViewEngines.Engines.Add(engine);

  • #
    ‫۹ سال و ۴ ماه قبل، دوشنبه ۲۱ اردیبهشت ۱۳۹۴، ساعت ۰۳:۵۳
    با سلام و تشکر؛
    آیا برای ایجاد یک سیستم مدیریت محتوا یا همون Cms میشه از این روش استفاده کرد یا اینکه باید از Mef هم استفاده بشه. ؟
    آیا میشه فقط از همین روش استفاده کرد ؟ آیا میشه فقط از Mef استفاده کرد یا اینکه هردوش؟
    آیا میشه هر افزونه رو به صورت نصبی تو سیستم اصلی تزریق کرد؟ یعنی یه صفحه add-on اضافه کنیم و با انتخاب افزونه‌ها بشه تو سیستم نصب بشه؟
    لطفا روش ایجاد نصب یا منبعی اگه برای این کار وجود داره رو بفرمائید؟ 
    • #
      ‫۹ سال و ۴ ماه قبل، دوشنبه ۲۱ اردیبهشت ۱۳۹۴، ساعت ۰۴:۲۷
      - در طراحی جاری نیازی به MEF نیست. کار بارگذاری و تشخیص افزونه‌ها توسط استراکچرمپ انجام می‌شود. (پیشنیاز (ج) ابتدای بحث)
      - برای نصب افزونه‌های طراحی ارائه شده، فقط کافی است آن‌ها را به پوشه‌ی bin کپی کنید (اولین نظر بحث جاری).
  • #
    ‫۹ سال و ۴ ماه قبل، دوشنبه ۲۱ اردیبهشت ۱۳۹۴، ساعت ۰۴:۵۳

    ممنون از شما

    برای اضافه نمودن قابلیت چند زبانه (Globalization) به این سیستم نکته خاصی وجود داره ؟

    • #
      ‫۹ سال و ۴ ماه قبل، دوشنبه ۲۱ اردیبهشت ۱۳۹۴، ساعت ۰۵:۱۲
      از نکات مطلب «ASP.NET MVC #22» استفاده کنید (embedded resource و کامپایل شده هستند).  
  • #
    ‫۹ سال و ۴ ماه قبل، دوشنبه ۲۱ اردیبهشت ۱۳۹۴، ساعت ۲۳:۵۳

    اگه قرار باشه یک سایت سه بخش مجزا داشته باشه که هرکدوم دارای پلاگین‌های خودش باشه اونوقت باید هرکدوم IplugIn جداگانه داشته باشه؟

    مثلا سه بخش Root ، User و   Admin اونوقت افزونه نویسی برای این بخش ها به چه شکل خواهد بود؟

    • #
      ‫۹ سال و ۴ ماه قبل، سه‌شنبه ۲۲ اردیبهشت ۱۳۹۴، ساعت ۰۰:۰۱
      - مثال نهایی این سه قسمت دارای دو افزونه است. کدهای نهایی آن‌را پس از مطالعه‌ی هر سه قسمت، بررسی کنید.
      - ساختار تمام افزونه‌های دیگر هم مانند افزونه‌ی توضیح داده شده‌است. قسمت «بارگذاری و تشخیص خودکار افزونه‌ها » در مطلب، اساسا کاری به محل قرارگیری یا نحوه‌ی تعریف افزونه‌ها ندارد. فقط اسمبلی‌های موجود در پوشه‌ی bin برنامه‌ی اصلی (فایل‌های dll نهایی) را اسکن می‌کند و بر اساس قرارداد مشخص شده، آن‌ها را به سیستم اضافه خواهد کرد. بنابراین مهم نیست که این افزونه‌ها جزئی از پروژه‌ی جاری هستند یا خیر. آیا توسط یک تیم دیگر در سیستم‌های مستقلی در حال تهیه هستند یا خیر. همینقدر که فایل dll نهایی این افزونه‌ها را در پوشه‌ی bin برنامه‌ی اصلی کپی کنید، کار اسکن خودکار آن‌ها توسط استراکچرمپ انجام خواهد شد.
  • #
    ‫۹ سال و ۱ ماه قبل، سه‌شنبه ۱۰ شهریور ۱۳۹۴، ساعت ۱۴:۲۲
    با سلام.
    من هر کاری میکنم وقتی روی لینک مربوط به افزونه کلیک میکنم View مربوط به برنامه اصلی و نشون میده !
       public MenuItem GetMenuItem(RequestContext requestContext)
            {
                return new MenuItem
                {
                    Name = "بیوگرافی",
                    Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "Biography"}),
                    Icon = "fa fa-child"
                };
            }
     public class BiographyAreaRegistration : AreaRegistration 
        {
            public override string AreaName 
            {
                get 
                {
                    return "Biography";
                }
            }
    
            public override void RegisterArea(AreaRegistrationContext context) 
            {
                context.MapRoute(
                    "Biography_default",
                    "Biography/{controller}/{action}/{id}",
                   new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                    namespaces:new []{$"{this.GetType().Namespace}.Controllers"}
                );
            }

    • #
      ‫۹ سال و ۱ ماه قبل، سه‌شنبه ۱۰ شهریور ۱۳۹۴، ساعت ۱۴:۳۸
      اول سؤال قسمت دوم هم، همین هست و پاسخ گرفته.
    • #
      ‫۹ سال و ۱ ماه قبل، سه‌شنبه ۱۰ شهریور ۱۳۹۴، ساعت ۱۶:۵۹
      اینم خطایی که میده!
      The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
      ~/Areas/Biography/Views/My/Index.cshtml
      ~/Areas/Biography/Views/Shared/Index.cshtml
      ~/Views/My/Index.cshtml
      ~/Views/Shared/Index.cshtml
      ~/Areas/Biography/Views/My/Index.vbhtml
      ~/Areas/Biography/Views/Shared/Index.vbhtml
      ~/Views/My/Index.vbhtml
      ~/Views/Shared/Index.vbhtml
      چرا نباید پیدا کنه!
      دوستان لطفا کمک کنید
      • #
        ‫۹ سال و ۱ ماه قبل، سه‌شنبه ۱۰ شهریور ۱۳۹۴، ساعت ۱۷:۱۰
        چندتا break point باید تعریف کنی. یکی داخل AreaRegistration تا مشخص بشه این مورد فراخوانی میشه؟ مورد دیگه داخل PluginsStart و دیگری هم داخل جایی که RazorGeneratorMvcStart تعریف شده. این موارد اگر ثبت نشن، خطایی رو که عنوان کردی دریافت می‌کنی. کار RazorGeneratorMvcStart این هست که Viewها رو از اسمبلی بخونه، نه از فایل سیستم که خطاش رو ارسال کردی و اگر این خطا رو می‌ده یعنی درست ثبت نشده و خونده نشده.
        • #
          ‫۹ سال و ۱ ماه قبل، سه‌شنبه ۱۰ شهریور ۱۳۹۴، ساعت ۱۷:۱۵
          ممنون از پاسخ شما .
          تو همشون break point گذاشتم و همشون اجرا میشن و ثبت میشن.
          نمیدونم مشکل از کجاست! 
        • #
          ‫۹ سال و ۱ ماه قبل، سه‌شنبه ۱۰ شهریور ۱۳۹۴، ساعت ۱۷:۱۸
          از RazorGenerator.Mvc.2.3.6  j تو پروژم استفاده کردم - ویژوال استودیو 2015 هست. نمیدونم ربطی به این موارد داره یا نه !؟
      • #
        ‫۹ سال و ۱ ماه قبل، سه‌شنبه ۱۰ شهریور ۱۳۹۴، ساعت ۱۸:۴۳
        این مورد به دلیل این است که Custom Tool مربوط به ویو Index را فراموش کرده باشید به RazorGenerator تنظیم کنید. یا اگر تنظیم کرده اید دقت کنید که کلاس مورد نظر ایجاد شده باشد. اگر نه با Save هر باره ویو این کلاس ساخته میشود.
  • #
    ‫۹ سال و ۱ ماه قبل، چهارشنبه ۱۱ شهریور ۱۳۹۴، ساعت ۱۵:۵۴
    با سلام مجدد.
    زمانی که تو کنسول package manager از دستور Enable-RazorGenerator استفاده میکنم پیغام خطای زیر رو بهم نشون میده. ممکنه مشکل بنده هم همین باشه. ممنون میشم توضیح بدید این چی میگه! و باید چیکار کنم. با تشکر از لطف شما
    Exception calling "GetItem" with "1" argument(s): "Value does not fall within the expected range."
    At F:\projects\MvcProject\PPU\packages\RazorGenerator.Mvc.2.3.6\tools\RazorGenerator.psm1:63 char:21
    + ...             $solutionExplorer.GetItem("$SolutionName\$ProjectName$rel ...
    +                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
        + FullyQualifiedErrorId : ArgumentException

      • #
        ‫۹ سال و ۱ ماه قبل، چهارشنبه ۱۱ شهریور ۱۳۹۴، ساعت ۱۷:۴۶
        واقعا ممنونم از شما
        مشکلم حل شده - منتها فقط view‌های کنترلر Home افزونه و نمیشناسه و صفحه home پروژه اصلی و نشون میده !
        • #
          ‫۹ سال قبل، شنبه ۲۱ شهریور ۱۳۹۴، ساعت ۱۸:۵۸
          مشکل شما را بنده هم داشتم
          لینک زیر را چک نمایید و از NET4.5 استفاده کنید
          http://stackoverflow.com/questions/10608948/visual-studio-cannot-find-custom-tool-razorgenerator?answertab=active#tab-top 
          • #
            ‫۹ سال قبل، شنبه ۲۱ شهریور ۱۳۹۴، ساعت ۲۰:۲۸
            پیشنیاز (ب) ابتدای مطلب را یکبار مطالعه کنید.
  • #
    ‫۸ سال و ۱۰ ماه قبل، پنجشنبه ۵ آذر ۱۳۹۴، ساعت ۲۰:۳۲
    سلام؛ اگه بخوایم یه PartialView رو از یه پلاگینی رو تو صفحه اول برنام اصلی (Layout) رندر کنیم چکار باید کرد. شاید اینطور بشه تفسیرش کرد که بخوایم پلاگینی رو به عنوان یه Widget تو صفحه اصلی رندر کنیم. ممنون میشم اگه بفرمائید باید چکار کرد؟
    • #
      ‫۸ سال و ۱۰ ماه قبل، پنجشنبه ۵ آذر ۱۳۹۴، ساعت ۲۳:۱۷
      از Html.RenderAction استفاده کنید (قسمت نمایش اطلاعات از کنترلر‌های مختلف در یک صفحه). 
  • #
    ‫۸ سال و ۸ ماه قبل، یکشنبه ۲۰ دی ۱۳۹۴، ساعت ۱۷:۲۷
    با سلام و عرض تشکر بابت مقاله بسیار مفیدتون.
    با توجه به اینکه اینترفیس IContainer در کتابخانه StructureMap است. به نظر می‌رسد نیاز است بسته نوگت زیر نیز روی همه پروژه‌های سلوشن نصب گردد.
    PM> install-package structuremap
    یا حق
  • #
    ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۲ شهریور ۱۳۹۵، ساعت ۰۵:۳۰
    با سلام، من از Automapper 5.0.2 استفاده میکنم، در پروژه‌ی اصلی استفاده کردم و مشکلی ندارم. به درستی Profile‌ها در کلاس Registry اضافه میشن ولی برای استفاده از Automapper در پروژه‌ی پلاگین به خطای نبودن Profile میخورم. من در قسمت پلاگین مانند پروژه‌ی اصلی، از class library جدا برای ایجاد Profile و یک Registery جدا برای آن، استفاده میکنم.
    در این حالت چون دوتا Registry ایجاد شده (یکی برای پلاگین و یکی برای پروژه‌ی اصلی)، فکر میکنم Profile‌ها جایگزین می‌شوند.
    چطور میتونم Profile‌های پلاگین رو اضافه کنم؟ آیا میشه از یک Registry برای اضافه کردن Profile‌های پلاگین استفاده کنم؟
      • #
        ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۲ شهریور ۱۳۹۵، ساعت ۰۵:۴۲
        بله دقیقا بنده از همین کد استفاده کردم، ولی نتونستم Profile‌های پلاگین رو اضافه کنم، Profile‌های پلاگین در Class library جدا از پروژه‌ی اصلی و در پوشه‌ی Plugins وجود داره
        چطور می‌تونم assembly پلاگین رو پیدا کنم تا بتونم Profile‌های پلاگین رو اضافه کنم
        • #
          ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۲ شهریور ۱۳۹۵، ساعت ۰۵:۵۱
          از PluginsStart ایده بگیرید که چطور پلاگین‌ها را پیدا می‌کند و چطور متدهای آن‌را فراخوانی می‌کند. اگر IPlugin نیاز به متدهای بیشتری برای تنظیمات اولیه‌ی پلاگین‌ها دارد، به آن اضافه کنید.
          • #
            ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۲ شهریور ۱۳۹۵، ساعت ۱۵:۱۰
            خب من با همین روشی که گفتید Profile‌های پلاگین رو بدست آوردم ولی هر کاری میکنم نمی‌تونم Profile‌ها رو به MapperConfiguration اضافه کنم! 
            آیا با Structuremap می‌تونم Instance ی که از MapperConfiguration  در رجیستری AutoMapperRegistry  ساخته شده رو بگیرم و Profile  رو بهش اضافه کنم؟
            • #
              ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۲ شهریور ۱۳۹۵، ساعت ۱۵:۲۱
              - پیشنیازهای مطلب را یکبار مطالعه کنید؛ خصوصا مورد «اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap» که در آن  scanner.AddAllTypesOf ذکر شده‌است. در آنجا IPlugin ثبت شده‌است؛ شما Profile یا هر نوع دیگری را که نیاز است به آن اضافه کنید.
              -روش دوم: در مثالی که ذکر شده، تک اسمبلی جاری بررسی می‌شود. برای یافتن تمام اسمبلی‌های بارگذاری شده‌ی در برنامه (منجمله پلاگین‌ها) از متد ()AppDomain.CurrentDomain.GetAssemblies استفاده کنید.
              • #
                ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۲ شهریور ۱۳۹۵، ساعت ۱۷:۲۱
                ممنون، از روش اول استفاده کردم، جواب داد
                public class AutoMapperRegistry : Registry
                    {
                        public AutoMapperRegistry()
                        {
                
                            Scan(scanner =>
                            {
                                scanner.TheCallingAssembly();
                                scanner.AddAllTypesOf<Profile>();
                            });
                
                            For<MapperConfiguration>().Use("", ctx =>
                            {
                                var profiles=ctx.GetAllInstances<Profile>().ToList();
                                var config = new MapperConfiguration(cfg =>
                                {
                                    foreach (var profile in profiles)
                                    {
                                        cfg.AddProfile(profile);
                                    }
                                });
                                return config;
                            });
                            For<IMapper>().Use(ctx => ctx.GetInstance<MapperConfiguration>().CreateMapper(ctx.GetInstance));
                        }
                    }

  • #
    ‫۵ سال و ۷ ماه قبل، سه‌شنبه ۲ بهمن ۱۳۹۷، ساعت ۱۲:۴۸
    با سلام؛ من هر سه قسمت رو انجام دادم اما زمانیکه اجرا میزنم این یک تیکه از کد خطای میده. 
    Method not found: 'Void StructureMap.Graph.IAssemblyScanner.AssembliesFromPath(System.String, System.Func`2<System.Reflection.Assembly,Boolean>)'.'
    اگر لطف کنید راهنمایی کنید ممنون میشم

     
    • #
      ‫۵ سال و ۷ ماه قبل، سه‌شنبه ۲ بهمن ۱۳۹۷، ساعت ۱۲:۵۸
      یا عدم تطابق شماره نگارش‌های استراکچرمپ را در پروژه‌های مختلف دارید و یا استراکچرمپ در پروژه‌ای که باید ارجاعی به آن وجود داشته باشد، نصب نشده. این لیست را دقیق بررسی کنید.