کار با Partial View و Partial Model در MVC

سه شنبه 14 اردیبهشت 1395

در این مقاله به معرفی Partial View ها می پردازیم و همچنین نحوه ی استفاده از Partial model ها را درASP.NET MVC توضیح می دهیم. همچنین در پایان مقاله، کد ها به صورت یک فایل فشرده در اختیار شما گذاشته شده اند.

کار با Partial View و Partial Model  در MVC

 مقدمه

یکی از مشکلاتی که  در زمینه ی partial view  ها داریم این است که با این که محتوای view  از المان های صفحه تامین می شود، باز هم ناچاریم صفحه را برای بارگذاری اطلاعات،  دوباره Post back  کنیم.   این مقاله در تلاش است  تا راه حلی برای این مشکل پیدا کند. استفاده ی دوباره از کدهای قبلی ، می تواند تا حد زیادی در زمان هر برنامه نویسی صرفه جویی کند. در حوزه برنامه نویسی وب ، این یک روش معمول است که برنامه نویسان از یک تکه کد ، چنین بار در طول پروژه ها استفاده می کنند. ما نیز در این مثال،  از HTML mark-up code ها چندین بار استفاده کرده ایم. ASP.NET MVC  دارای ویژگی های متعددی از جمله Partial View ها ،Child Action ها و قالب های ویرایش/نمایش است که این موارد می توانند در حل مشکل برای ما بسیار مفید باشند. Partial View ها می توانند از Page Model ها به عنوان منابع داده بهره بگیرند زیرا Child Action ها در قسمت داده و اطلاعات از Controller ها مستقل هستند. قالب های ویرایش/نمایش،  آیتم ها را از model  به سیستم منتقل می کنند و می توانند توسط partial view  هایی که کاربر می سازد، تغییر کنند و یا حتی از کار بیفتند . این مقاله ، بایک مثال ، به بررسی اجمالی ای بر روی مشکلی که در زمان post back داده ها از Partial View رخ می دهد ، می پردازد.

در این مثال با استفاده از Partial view ، سایت یک شرکت را بارگذاری می کنیم. وقتی آدرس پستی شرکت به عنوان یک Partial view  در نظر گرفته شود، می تواند در صفحات بعدی به عنوان یک آدرس فروش به کار برود. اگر یک بار موفق بشویم که این مشکل راحل کنیم ، صفحات بزرگ می توانند به وسیله ی شکسته شدن به بخش های کوچک تر ، مدیریت آسان تری پیدا کنند.

پیشینه

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

وقتی با Microsoft MVC برای طراحی صفحات وب کار می کنیم، اکثر ویژگی ها به صورت پیش فرض در آن گنجانده شده اند اما ممکن است گاهی نیاز داشته باشید تا چیزی را به آن اضافه کنید. محیط یک form  یکی از این موارد است . Microsoft از HTML helper ها برای ایجاد المان های موجود در فرم استفاده می کند. انجام این کار به صورت دستی ، مدت زمان زیادی طول خواهد کشید و ممکن است خیلی از موارد نیز از قلم بیفتند. خوشبختانه این موارد برای توضیح، آسان هستند و مثال های زیادی از آن ها در اینترنت یافت می شود.

در این مقاله یک مشکل را مطرح می کنیم. در ابتدا نگاهی به تلاش های دیگران برای حل این مشکل می پردازیم و سپس یک HTML helper جدید تولید می کنیم که مشکل را برای ما حل می کند.

مشکل

ابتدا اجازه بدهید به الگوی MVC Controller استاندارد که برای فرم ها استفاده می شود، نگاهی بیندازیم.

[HttpGet]
public ActionResult Index()
{
    // load the test data
    TestViewModel model = new TestViewModel();
    return View(model);
}

[HttpPost]
public ActionResult Index(TestViewModel model)
{
    if (ModelState.IsValid)
    {
        // save the test data
    }
    return View(model);
}

HTTP action ، داده ها را در درون یک model بارگذاری می کند و سپس یک view  را فراخوانی می کند تا آن داده ها را نمایش بدهد. view داده ها را در یک form نمایش می دهد که کاربر می تواند آن را ویرایش کند.

با کلیک بر روی یک دکمه ی submit ، اطلاعات به HttpPost action کنترلر بر می گردند ، در همین زمان، داده ها نیز از روی form به model  متصل شده اند. کنترلر ، مدل را از نظر اعتبار  می سنجد و اگر همه ی اجزای مدل ، درست بودند، داده ها را ذخیره می کند.  controller سپس یک view  مشابه برای نمایش اطلاعات ویرایش شده فراخوانی می کند . توجه داشته باشید که در هر دو action از یک مدل استفاده شده است و استفاده از این الگو تضمین می کند که data binding (اتصال داده ها) همیشه کار خواهد کرد حتی برای مدل های پیچیده که شامل کلاس ها و یا لیست های دیگر هستند. در حقیقت، این موضوع، یکی از کارهای هوشمندانه ای است که اگر از این الگو استفاده کنیم ، MVC برای ما انجام می دهد.

در اینجا یک مدل نمونه آورده شده است :

public class TestViewModel
{
    public TestModel Test { get; set;}
}

public class TestModel
{
    [Display(Name = "Name:")]
    [Required(ErrorMessage = "Please provide a name")]
    public string Name { get; set; }
    public TestPartialModel Partial { get; set; }
}

public class TestPartialModel
{
    [Display(Name="Partial Name:")]
    [Required(ErrorMessage="Please provide a name")]
    public string Name { get; set; }
}

و در زیر نیز view آورده شده است :

@model CSE.Partial.WebApp.Models.TestViewModel
@{
  ViewBag.Title = "Test";
}
<h2>@ViewBag.Title</h2>
@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()

  <div>
    <hr />
    <dl class="dl-horizontal">
      <dt>@Html.LabelFor(m => m.Test.Name)</dt>
      <dd>
        @Html.EditorFor(m => m.Test.Name)
        @Html.ValidationMessageFor(m => m.Test.Name)
      </dd>

      <dt>@Html.LabelFor(m => m.Test.Partial.Name)</dt>
      <dd>
        @Html.EditorFor(m => m.Test.Partial.Name)
        @Html.ValidationMessageFor(m => m.Test.Partial.Name)
      </dd>

      <dt></dt>
      <dd><input class="btn btn-primary" type="submit" value="Save" /></dd>
    </dl>
  </div>
}

این کد همان طور که انتظار داریم، کار می کند و وقتی دکمه ذخیره را میزنیم، همه ی مقادیر ورودی به کنترلر ، Post back  می شوند و view جدید بر اساس آن مقادیر برگردانده می شود.

گام بعدی ،منتقل کردن Partial bit به یک Partial view است.

TestPartial_ می تواند در یک پوشه view مشابه و یا در پوشه ی Shared باشد.

 @model CSE.Partial.WebApp.Models.TestViewModel
<dt>@Html.LabelFor(m => m.Test.Partial.Name)</dt>
<dd>
  @Html.EditorFor(m => m.Test.Partial.Name)
  @Html.ValidationMessageFor(m => m.Test.Partial.Name)
</dd>

و حالا view به شکل زیر است :

@model CSE.Partial.WebApp.Models.TestViewModel
@{
  ViewBag.Title = "Test";
}
<h2>@ViewBag.Title</h2>
@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()
   <div>
     <hr />
     <dl class="dl-horizontal">
       <dt>@Html.LabelFor(m => m.Test.Name)</dt>
       <dd>
          @Html.EditorFor(m => m.Test.Name)
          @Html.ValidationMessageFor(m => m.Test.Name)
        </dd>

        @Html.Partial("_TestPartial")

       <dt></dt>
       <dd><input class="btn btn-primary" type="submit" value="Save" /></dd>
    </dl>
  </div>
}

این تکه کد درست کار می کند به طوری که تمام model به صورت پیش فرض به Partial view  منتقل می شود. اما مامی خواهیم که Partial view دوباره قابل استفاده باشد ولی همان طور که می بینیم در مثال بالا ، Partial view به صفحه model  متصل شده است.

بنابراین بیایید تلاش کنیم Partial model  را به Partial view  منتقل کنیم. Partial view را طوری تنظیم می کنیم که از مدل خودش استفاده کند:

@model CSE.Partial.Service.Models.TestPartialModel
<dt>@Html.LabelFor(m => m.Name)</dt>
<dd>
  @Html.EditorFor(m => m.Name)
  @Html.ValidationMessageFor(m => m.Name)
</dd>

مدل درست را از صفحه view منتقل کنید:

@Html.Partial("_TestPartial", Model.Test.Partial)

حالا وقتی صفحه را Post back می کنیم ، یک exception رخ می دهد . وقتی داده های Post back شده را بررسی می کنیم، متوجه می شویم که Partial model گم شده است. مشکل همینجاست!

حل مشکل

مشکل کجاست؟ اگر به صفحه HTML با زدن دکمه ی F12 و یا دیدن view source  برویم، به صورت آشکاری می بینیم که المان name که خودش به صورت پیش فرض تنظیم شده بود، در تست های جاری متفاوت از تست نهایی است.

این کار می کند:

<input name="Test.Partial.Name" id="Test_Partial_Name" type="text" value="">

این با خطا روبرو می شود:

<input name="Name" id="Name" type="text" value="">

 

پیشوند اسم در partial HTML نیست.

اگر نگاهی به انجمن ها بیندازیم می بینیم که بقیه ی افراد هم برای حل این مشکل تلاش کرده اند و با نگاه دقیق تری به آن ها متوجه می شویم که در MVC  کلاسی به اسم TemplateInfo وجود دارد که Property  ای به نام HtmlFieldPrefix دارد. اگر ما از این property قبل از فراخوانی partial view  استفاده کنیم، می توانیم تمام المان ها را مجبور کنیم که از پیشوند استفاده کنند.

@{ Html.ViewData.TemplateInfo.HtmlFieldPrefix = "Test.Partial"; }
@Html.Partial("_TestPartial", Model.Test.Partial)

 

این تکه کد در این برنامه کار می کند ولی همه ی المان ها بعد از partial هم ، پیشوند خواهند گرفت.

پارامتر سومی در @Html.Partial( , , ViewDataDictionary) وجود دارد . ما به وسیله ی آن  می توانیم یک ViewData  همراه با HtmlFieldPrefix به بیرون از  partial view منتقل کنیم.(مطابق کدی که در بالا می بینیم.)

@Html.Partial("_TestPartial", Model.Test.Partial, new ViewDataDictionary
{
  TemplateInfo = new System.Web.Mvc.TemplateInfo
  {
    HtmlFieldPrefix = "Test.Partial"
  }
})

این کار، پیشوند ها را فقط به Partial  محدود می کند. اما حالا  ModelState و ViewData گم شده اند. پیام های خطا، حالا برای ظاهر شدن با مشکل روبرو خواهند شد. بسیاری از موارد که به صورت پیش فرض منتقل می شدند، حالا در پارامتر سوم دیگر در دسترس نیستند. اگر ما ابتدا یک کپی از view data بگیریم و سپس TemplateInfo را ویرایش کنیم ، باید مشکل حل شود. سازنده ی ViewDataDictionary ، برای ما عمل کپی کردن  Html.ViewData را انجام می دهد.

@Html.Partial("_TestPartial",
Model.Test.Partial,
new ViewDataDictionary(Html.ViewData)
{
  TemplateInfo = new System.Web.Mvc.TemplateInfo
  {
    HtmlFieldPrefix = "Test.Partial"
  }
})

هنوز یک مشکل باقی مانده است.  اگر از این تکنیک برای Partial های تو در تو استفاده کنیم، پیشوند، باید قبل از تعریف متغیر ها به آن ها داده بشود. ما یک متد در MVC برای این کار پیدا کردیم. بنابراین راه حل کارآمد برای این مشکل عبارت است از:


  string name = Html.NameFor(m => m.Test.Partial).ToString();
  string prefix = Html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
  ViewDataDictionary viewData = new ViewDataDictionary(Html.ViewData)
  {
      TemplateInfo = new TemplateInfo { HtmlFieldPrefix = prefix }
  };
}

@Html.Partial("_TestPartial", Model.Test.Partial, viewData)

این ترفند کار ما را راه می اندازد اما کمی کثیف و در هم ریخته است و باید خط های زیادی از این کد را در مقابل هر  Html.Partial قرار بدهیم. حالا که راه حل را پیدا کردیم، بیایید آن را به یک HTML helper تبدیل کنیم. ما به دنبال این راه بودیم :

@Html.PartialFor(m => m.Test.Partial)

در اینجا یک extension method داریم. به تکه های lambda توجه کنید. این یک راه مطمئن برای انتقال مقادیر و همچنین نام ها به partial model است. نام partial view می تواند به عنوان یگ پارامتر پاس داده شود و یا می تواند در یک کلاس به نام TProperty و یا یک UIHint("template name") قرار بگیرد.

/// <summary>
/// Return Partial View.
/// The element naming convention is maintained in the partial view by setting the prefix name from the expression.
/// The name of the view (by default) is the class name of the Property or a UIHint("partial name").
/// @Html.PartialFor(m => m.Address)  - partial view name is the class name of the Address property.
/// </summary>
/// <param name="expression">Model expression for the prefix name (m => m.Address)</param>
/// <returns>Partial View as Mvc string</returns>
public static MvcHtmlString PartialFor<tmodel, tproperty>(this HtmlHelper<tmodel> html,
    Expression<func<TModel, TProperty>> expression)
{
    return html.PartialFor(expression, null);
}

/// <summary>
/// Return Partial View.
/// The element naming convention is maintained in the partial view by setting the prefix name from the expression.
/// </summary>
/// <param name="partialName">Partial View Name</param>
/// <param name="expression">Model expression for the prefix name (m => m.Group[2])</param>
/// <returns>Partial View as Mvc string</returns>
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> html,
    Expression<Func<TModel, TProperty>> expression,
    string partialName
    )
{
    string name = ExpressionHelper.GetExpressionText(expression);
    string modelName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
    ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
    object model = metaData.Model;


    if (partialName == null)
    {
        partialName = metaData.TemplateHint == null
            ? typeof(TProperty).Name    // Class name
            : metaData.TemplateHint;    // UIHint("template name")
    }

    // Use a ViewData copy with a new TemplateInfo with the prefix set
    ViewDataDictionary viewData = new ViewDataDictionary(html.ViewData)
    {
        TemplateInfo = new TemplateInfo { HtmlFieldPrefix = modelName }
    };

    // Call standard MVC Partial
    return html.Partial(partialName, model, viewData);
}

دو تا متد از MVC  نیز باید اضافه شوند:

ExpressionHelper.GetExpressionText gets the name of the expression m => m.Test.Partial would return "Test.Partial".
ModelMetadata.FromLambdaExpression gets the value of the expression. e.g. the model Partial.

قالب ویرایش/نمایش 

بعد از حل مشکل بالا ، بهتر است نگاهی به قالب های ویرایش گر بیندازیم.  مطابق یک نقل قول از Microsoft MSDN :

متد EditorFor برای ایجاد MVCHtmlString mark-up بر پایه نوع داده ای که دریافت می کند، به کار می رود.

مثال های زیادی از این موضوع در وب وجود دارد. کاری که این متد انجام می دهد عبارت است از : این متد به تنهایی اجرا می شود و خروجی هایش را به گونه ای تنظیم می کند که با نوع Property ها منطبق باشد و همچنین از metadata  های Property  نیز برای افزودن جزئیات بیشتر استفاده می کند.( property attributes

به عنوان مثال ، این متد، یک text box  برای نوع string  و یک checkbox  برای نوع  bool نشان می دهد. این متد، نوع ورودی را به یک Html5 attribute مناسب اختصاص می دهد. وقتی شما از یک شی استفاده می کنید، این متد به همراه label  ها و المان های ورودی ، یک <div> برای آن ها قرار می دهد. نکته ی جالبی که وجود دارد این است که شما می توانید بر روی این رفتارهای پیش فرض، بازنویسی کنید. برای این کار نیاز به اضافه کردن یک پوشه دارید به اسم EditorTemplates و یا DisplayTemplates . این پوشه را باید در پوشه ی view و یا پوشه ی Shared قرار بدهید. در این مورد شما می توانید بر روی  partial view ، نام نوعی که دارید باز نویسی می کنید را بدهید. این کار هم برای نوع های ساده و هم برای نوع های پیچیده قابل انجام است. ما این کار را بر روی پروژه ی بالا انجام دادیم و به طرز جادویی ای، مثل PartialFor extension method کار کرد. این کار همچنین باعث شد تا پیشوند ها به درون لایه ها نیز بروند. 

در پروژه (که در بالا به صورت فشرده قرار داده شده است) نام همه ی partial ها را با نام model class آن ها قرار داد و آن ها رادر پوشه های ViewName/DisplayTemplate  و Shared/DisplayTemplate قرار دادم. شما می توانید در دفعات بعدی از آن ها به شکل زیر استفاده کنید:


@Html.EditorFor(m => m.Partial.First)

Html.Partial اینطور انتظار دارد که partial view در پوشه ی view  جاری و یا در پوشه ی Shared  قرار داشته باشد و نه در پوشه دیگری. برای تست کردن این مساله ما هم با استفاده از یک پارامتر به نام Partial ، از یک partial view مشابه برای EditorFor استفاده کردیم.

@Html.PartialFor(m => m.Partial.First, "EditorTemplates/FirstPartialModel")

PartialFor و EditorFor

تفاوت بین این دو چیست؟ تفاوت چندان زیادی وجود ندارد. تفاوت اصلی در مکان partial view ها است.

-PartialFor : همانند Partial است، در یک پوشه ی view  جاری و یا در پوشه ی Shared

-EditorFor : در قالب Editor ،در یک پوشه ی زیر مجموعه از view  و یا پوشه Shared

ما debugger را اجرا کردیم و کد MVC EditorFor را بررسی کردیم. این مورد ، محل ذخیره و سپس نوع را چک می کند که یک کلاس پیچیده هست یا نه.  داده های موجود در View راکپی می کند ، پیشوند اضافه را بر روی TemplateInfo تنظیم می کند و کد partial view مشابه را فراخوانی می کند، دقیقا مشابه کاری که @Html.Partial انجام می دهد. اگر شما در اینترنت درباره ی "html.editorfor vs partial view" جستجو کنید ، مطالب زیادی خواهید دید. یکی از مزایای PartialFor زمانی است که شما می خواهید بر روی یک مکان دقیق از partial view کنترل داشته باشید .

PartialFor همچنین با ارث بری ها، مجموعه ها و interface ها نیز کار می کند.

@Html.PartialFor(expression, partialName); partialName می تواند یک نام، یک مسیرکامل و یا یک مسیر وابسته باشد.

  @HtmlEditorFor(expression, templateName); templateName

می تواند یک نام و یا یک مسیر وابسته باشد. "EditorTemplates" به صورت hard coded در source code پروژه قرار دارد.

Interface ها و ارث بری ها

نویسنده همچنین در ادامه درباره ی این موضوع می گوید:

وقتی این دانش را فرا گرفتم، شروع به استفاده از قالب ها برای تعریف view  برای شی هایم کردم تا بتوانم از آن ها در صفحات و فرم ها بهره بگیرم.همه چیز خیلی خوب پیش رفت تا این که من سعی کردم بخش ارث بری شده ی کلاس را به partial view بخش کنم. من اینطور تصور می کردم که این راه، کاندید خوبی برای استفاده ی دوباره از کد است. EditorFor هیچ چیزی را به عنوان خروجی برنگرداند در حالی که PartrialFor به خوبی عمل کرد. در مراحل دیباگ و بررسی کد، این نکته آشکار شد که کد بخش EditorFor برای شی هایی که قبلا یک بار بررسی شده اند، در حال بررسی بود و بخش ارث بری شی را نمی پذیرفت.  این ویزگی اساسا در حال تلاش برای جلوگیری از ایجاد بازگشت است ولی در مقاله ی ما، بازگشتی وجود ندارد. فقط در اینجا این ویژگی بخش های ارث بری شده و مشتق شده از شی ها را در قالب partial view های جداگانه برمی گرداند. بنابراین من خوشحالم که نسخه ی PartialFor را پاک نکردم.

Partial View  و ChildAction و EditorFor


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

استفاده از کد

برای اجرای این solution ، به Microsoft Visual Studio 2013 نیاز خواهید داشت. پروژه ی نمونه در مقاله قرار داده شده است و شما می توانید ان را دانلود کنید. برای این که پروژه برای شما به درستی کار کند،نیاز دارید تا از ویژگی NuGet packages استفاده کنید. ممکن است بسته به نسخه ی برنامه ی Visual Studio ، این ویژگی به صورت خودکار برای شما نصب شود. اگر این اتفاق نیفتاد به روش زیر عمل کنید:

بر روی solution کلیک راست کنید و گزینه ی Enable Nuget Package Restore را انتخاب کنید.

به قسمت Tools/Options/NuGet Package Manager/General Package Restore بروید و مطمئن بشوید که به Nuget اجازه دانلود قسمت های از دست رفته را داده اید.

در منوی آیتم یک لیست آبشاری می بینید که شامل Partial نیز هست، با کلیک کردن بر روی این گزینه ، سه view  خواهید دید. 

آدرس پستی یک فرم،  شامل جزئیات شرکت و یک آدرس پستی است. آدرس پستی در یک partial view تعبیه شده است. این یک نمونه ی ساده است ولی مجزا بودن partial view ها را به خوبی به شما نشان می دهد.

Partial های تو در تو : یک صفحه است که Partial ها را در درون یکدیگر بررسی می کند و به این ترتیب نشان می دهد که اتصال داده ها در سطحی بالاتر از یک سطح نیز کار می کند. همچنین ثابت می کند که فرآیند خطاگیری ModelState همچنان در طول پروژه برقرار است و ViewData به Partial منتقل می شود. راه حل های جایگزین دیگری در انجمن های اینترنتی دیده می شوند که کارآمد نیستند. 

 

نکات مهم 

نکته ی مهم این است که الگوی controller ای که در مثال بالا استفاده شده است، بسیار عالی است و اتصال بین داده ها به طرز شگفت انگیزی ، خوب عمل می کند. بررسی جزء به جزء این برنامه به درک آن، کمک زیادی می کند. در حقیقت،  بدون بررسی جزء به جزء اعضای برنامه به این موفقیت و کشف راه حل نمی توان رسید. فضای نام CSE از اطلاعات پایه ای موجود در Cambridge Software Engineering برگرفته شده است.

ضمیمه

به سمت کد های .NET framework  می رویم.

دستورالعمل هایی برای Symbol/source server از سوی MSDN ارائه شده است. 

 برای این که تنظیمات Visual Studio  را برای استفاده در کاربرد های symbol/server به کار بگیرید، باید مطابق آن چه که در زیر گفته شده است، برنامه را پیکر بندی کنید.

1.Tools -> Options -> Debugger -> General بروید.

2."Enable Just My Code (Managed only)" را از حالت انتخاب خارج کنید. 

3."Enable .NET Framework source stepping" را از حالت انتخاب خارج کنید. بله ، این کار ممکن است باعث شود که برنامه ی شما به درستی کار نکند ولی عدم انجام این کار باعث می شود که سیستم ، دستورات سفارشی و دلخواه شما را نادیده بگیرد.

4."Enable source server support" را انتخاب کنید.

5."Require source files to exactly match the original version" را از حالت انتخاب خارج کنید.

6.Tools -> Options -> Debugger -> Symbols بروید.

7.symbol/source cache های محلی انتخاب کنید.

8.http://srv.symbolsource.org/pdb/Public ، ویژگی MVC را اضافه می کند.

9.مطابق آن چه در زیر آمده است، تنظیمات را انجام بدهید.

فایل های ضمیمه

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید