แนวทางปฏิบัติที่ดีที่สุดของ ViewModel


238

จากคำถามนี้ดูเหมือนว่าเหมาะสมที่จะให้ตัวควบคุมสร้างViewModelซึ่งสะท้อนรูปแบบที่มุมมองพยายามแสดงได้อย่างแม่นยำมากขึ้น แต่ฉันอยากรู้เกี่ยวกับอนุสัญญาบางอย่าง (ฉันใหม่กับรูปแบบ MVC ถ้ามันไม่ชัดเจน)

โดยทั่วไปฉันมีคำถามต่อไปนี้:

  1. ปกติฉันต้องการมีหนึ่งคลาส / ไฟล์ สิ่งนี้สมเหตุสมผลกับViewModel หรือไม่หากมันถูกสร้างขึ้นเพื่อส่งมอบข้อมูลจากคอนโทรลเลอร์ไปยังมุมมองเท่านั้น
  2. หากViewModelอยู่ในไฟล์ของตัวเองและคุณกำลังใช้โครงสร้างไดเรกทอรี / โครงการเพื่อแยกสิ่งต่าง ๆไฟล์ViewModelอยู่ที่ไหน ในไดเรกทอรีตัวควบคุม

สำหรับตอนนี้ ฉันอาจมีคำถามเพิ่มขึ้นอีกสองสามข้อ แต่สิ่งนี้รบกวนฉันในชั่วโมงสุดท้ายหรือมากกว่านั้นและฉันสามารถหาแนวทางที่สอดคล้องได้จากที่อื่น

แก้ไข: ดูตัวอย่างแอพ NerdDinnerบน CodePlex ดูเหมือนว่า ViewModels เป็นส่วนหนึ่งของControllersแต่มันก็ทำให้ฉันรู้สึกอึดอัดที่ไม่ได้อยู่ในไฟล์ของตัวเอง


66
ฉันจะไม่เรียกตัวอย่างของ NerdDinner ว่า "วิธีปฏิบัติที่ดีที่สุด" อย่างแน่นอน สัญชาตญาณของคุณทำหน้าที่คุณได้ดี :)
Ryan Montgomery

คำตอบ:


211

ฉันสร้างสิ่งที่ฉันเรียกว่า "ViewModel" สำหรับแต่ละมุมมอง ฉันวางไว้ในโฟลเดอร์ชื่อ ViewModels ในโครงการ MVC Web ของฉัน ฉันตั้งชื่อพวกเขาหลังจากตัวควบคุมและการกระทำ (หรือมุมมอง) ที่พวกเขาเป็นตัวแทน ดังนั้นหากฉันต้องการส่งผ่านข้อมูลไปยังมุมมอง SignUp บนคอนโทรลเลอร์ Membership ฉันสร้างคลาส MembershipSignUpViewModel.cs และวางไว้ในโฟลเดอร์ ViewModels

จากนั้นฉันจะเพิ่มคุณสมบัติและวิธีการที่จำเป็นในการถ่ายโอนข้อมูลจากคอนโทรลเลอร์ไปยังมุมมอง ฉันใช้ Automapper เพื่อรับจาก ViewModel ของฉันเป็น Domain Model และกลับมาอีกครั้งหากจำเป็น

สิ่งนี้ยังใช้งานได้ดีสำหรับคอมโพสิต ViewModels ที่มีคุณสมบัติที่เป็นประเภทของ ViewModels อื่น ๆ ตัวอย่างเช่นถ้าคุณมี 5 วิดเจ็ตบนหน้าดัชนีในตัวควบคุมความเป็นสมาชิกและคุณสร้าง ViewModel สำหรับแต่ละมุมมองบางส่วน - คุณจะส่งข้อมูลจากการกระทำดัชนีไปยังชิ้นส่วนได้อย่างไร คุณเพิ่มคุณสมบัติให้กับ MembershipIndexViewModel ชนิด MyPartialViewModel และเมื่อเรนเดอร์บางส่วนคุณจะส่งผ่านใน Model.MyPartialViewModel

การทำเช่นนี้ช่วยให้คุณปรับคุณสมบัติ ViewModel บางส่วนได้โดยไม่ต้องเปลี่ยนมุมมองดัชนีเลย มันยังเพิ่งผ่านใน Model.MyPartialViewModel ดังนั้นจึงมีโอกาสน้อยที่คุณจะต้องผ่านสายทั้งหมดของ partials เพื่อแก้ไขบางสิ่งบางอย่างเมื่อทุกสิ่งที่คุณทำคือการเพิ่มคุณสมบัติลงใน ViewModel บางส่วน

ฉันจะเพิ่มเนมสเปซ "MyProject.Web.ViewModels" ใน web.config เพื่อให้ฉันสามารถอ้างอิงพวกเขาในมุมมองใด ๆ โดยไม่ต้องเพิ่มคำสั่งการนำเข้าที่ชัดเจนในแต่ละมุมมอง เพียงแค่ทำให้มันสะอาดขึ้นเล็กน้อย


3
ถ้าคุณต้องการ POST จากมุมมองบางส่วนและส่งคืนมุมมองทั้งหมด (ในกรณีที่เกิดข้อผิดพลาดของโมเดล) ภายในมุมมองบางส่วนคุณไม่สามารถเข้าถึงโมเดลหลักได้
Cosmo

5
@Cosmo: จากนั้นให้โพสต์ไปยังการกระทำที่สามารถส่งคืนมุมมองทั้งหมดในกรณีที่เกิดข้อผิดพลาดของแบบจำลอง ทางฝั่งเซิร์ฟเวอร์คุณมีเพียงพอที่จะสร้างโมเดลพาเรนต์ใหม่
Tomas Aschan

การดำเนินการเกี่ยวกับการเข้าสู่ระบบ [POST] และการเข้าสู่ระบบ [GET] เป็นอย่างไร แตกต่างจาก viewmodels ไหม?
Bart Calixto

โดยปกติแล้วการเข้าสู่ระบบ [GET] จะไม่เรียกใช้ ViewModel เนื่องจากไม่จำเป็นต้องโหลดข้อมูลใด ๆ
Andre Figueiredo

คำแนะนำที่ดี การเข้าถึงข้อมูลการประมวลผลและการตั้งค่าคุณสมบัติรุ่น / VM ควรไปที่ไหน ในกรณีของฉันเราจะมีข้อมูลบางส่วนที่มาจากฐานข้อมูล CMS ในท้องที่และบางส่วนมาจากบริการบนเว็บซึ่งจะต้องดำเนินการ / จัดการก่อนที่จะตั้งค่าในแบบจำลอง การใส่ทุกอย่างในคอนโทรลเลอร์ทำให้ยุ่งมาก
xr280xr

124

การแยกคลาสตามหมวดหมู่ (ตัวควบคุม, ViewModels, ตัวกรองและอื่น ๆ ) นั้นไร้สาระ

หากคุณต้องการเขียนโค้ดสำหรับส่วน Home ของเว็บไซต์ของคุณ (/) ให้สร้างโฟลเดอร์ชื่อ Home และวาง HomeController, IndexViewModel, AboutViewModel เป็นต้นและคลาสที่เกี่ยวข้องทั้งหมดที่ใช้โดยแอคชัน Home

หากคุณมีคลาสที่แบ่งใช้เช่น ApplicationController คุณสามารถวางไว้ที่รูทของโปรเจ็กต์ของคุณ

ทำไมแยกสิ่งต่าง ๆ ที่เกี่ยวข้อง (HomeController, IndexViewModel) และเก็บสิ่งต่าง ๆ เข้าด้วยกันโดยไม่มีความสัมพันธ์เลย (HomeController, AccountController)


ฉันเขียนโพสต์บล็อกเกี่ยวกับหัวข้อนี้


13
สิ่งที่จะได้รับยุ่งสวยสวยได้อย่างรวดเร็วถ้าคุณทำเช่นนี้
UpTheCreek

14
ไม่ยุ่งคือการวางคอนโทรลเลอร์ทั้งหมดไว้ใน dir / namespace เดียว หากคุณมีตัวควบคุม 5 ตัวแต่ละตัวใช้ 5 viewmodels แล้วคุณจะได้ 25 viewmodels เนมสเปซเป็นกลไกสำหรับการจัดระเบียบโค้ดและไม่ควรแตกต่างจากที่นี่
Max Toro

41
@ Max Toro: คุณประหลาดใจที่ได้ลงคะแนนมาก หลังจากที่บางครั้งการทำงานใน ASP.Net MVC ฉันรู้สึกมากของความเจ็บปวดจากการมีทั้งหมด ViewModels ในสถานที่หนึ่งทุกตัวควบคุมในอีกและทุกมุมมองในอีก MVC เป็นสามส่วนของชิ้นส่วนที่เกี่ยวข้องพวกเขาเป็นคู่ - พวกเขาสนับสนุนซึ่งกันและกัน ฉันรู้สึกเหมือนการแก้ปัญหาสามารถฉันมากจัดขึ้นถ้าควบคุม ViewModels และชมสำหรับส่วนที่ได้รับร่วมกันอาศัยอยู่ในไดเรกทอรีเดียวกัน MyApp / Accounts / Controller.cs, MyApp / Accounts / Create / ViewModel.cs, MyApp / Accounts / Create / View.cshtml ฯลฯ
quentin-starin

13
@RyanJMcGowan การแยกข้อกังวลไม่ใช่การแยกชั้นเรียน
Max Toro

12
@RyanJMcGowan ไม่ว่าคุณจะเข้าถึงการพัฒนาปัญหาอย่างไรนั้นเป็นสิ่งที่คุณท้ายที่สุดโดยเฉพาะสำหรับแอพขนาดใหญ่ เมื่ออยู่ในโหมดบำรุงรักษาคุณไม่ต้องคำนึงถึงทุกรุ่นแล้วควบคุมทั้งหมดคุณเพิ่มฟังก์ชั่นหนึ่งครั้ง
Max Toro

21

ฉันเก็บคลาสแอปพลิเคชันของฉันไว้ในโฟลเดอร์ย่อยที่เรียกว่า "Core" (หรือไลบรารีแบบแยกส่วน) และใช้วิธีการเดียวกันกับแอปพลิเคชันตัวอย่างKIGGแต่มีการเปลี่ยนแปลงเล็กน้อยเพื่อทำให้แอปพลิเคชันของฉันแห้งมากขึ้น

ฉันสร้างคลาส BaseViewData ใน / Core / ViewData / ที่ฉันเก็บคุณสมบัติกว้างของไซต์ทั่วไป

หลังจากนี้ฉันยังสร้างคลาส ViewData ทั้งหมดของฉันในโฟลเดอร์เดียวกันซึ่งจากนั้นมาจาก BaseViewData และมีคุณสมบัติมุมมองที่เฉพาะเจาะจง

จากนั้นฉันสร้าง ApplicationController ที่ตัวควบคุมทั้งหมดของฉันได้รับมา ApplicationController มีวิธี GetViewData ทั่วไปดังนี้:

protected T GetViewData<T>() where T : BaseViewData, new()
    {
        var viewData = new T
        {
           Property1 = "value1",
           Property2 = this.Method() // in the ApplicationController
        };
        return viewData;
    }

ในที่สุดในการดำเนินการควบคุมของฉันฉันทำดังต่อไปนี้เพื่อสร้างรูปแบบ ViewData ของฉัน

public ActionResult Index(int? id)
    {
        var viewData = this.GetViewData<PageViewData>();
        viewData.Page = this.DataContext.getPage(id); // ApplicationController
        ViewData.Model = viewData;
        return View();
    }

ฉันคิดว่ามันใช้งานได้ดีจริง ๆ และทำให้มุมมองของคุณเป็นระเบียบเรียบร้อยและผู้ควบคุมของคุณผอม


13

คลาส ViewModel อยู่ที่นั่นเพื่อห่อหุ้มข้อมูลหลายชิ้นที่แสดงโดยอินสแตนซ์ของคลาสเป็นหนึ่งในการจัดการวัตถุที่ง่ายที่คุณสามารถส่งผ่านไปยังมุมมองของคุณ

มันสมเหตุสมผลแล้วที่จะมีคลาส ViewModel ของคุณในไฟล์ของตัวเองในไดเรกทอรีของตัวเอง ในโครงการของฉันฉันมีโฟลเดอร์ย่อยของโฟลเดอร์ Models ที่เรียกว่า ViewModels นั่นคือสิ่งที่ ViewModels ของฉัน (เช่นProductViewModel.cs) อยู่


13

ไม่มีสถานที่ที่ดีที่จะเก็บโมเดลของคุณไว้คุณสามารถแยกออกเป็นแอสเซมบลีแยกต่างหากถ้าโครงการมีขนาดใหญ่และมี ViewModels จำนวนมาก (ออบเจ็กต์การถ่ายโอนข้อมูล) นอกจากนี้คุณสามารถเก็บไว้ในโฟลเดอร์แยกต่างหากของโครงการไซต์ ตัวอย่างเช่นในOxiteพวกเขาจะอยู่ในโครงการ Oxite ซึ่งมีชั้นเรียนต่างๆมากมายเช่นกัน ตัวควบคุมใน Oxite จะถูกย้ายไปยังโครงการที่แยกต่างหากและมุมมองที่อยู่ในโครงการแยกต่างหากเช่นกัน
ในCodeCampServer ViewModels มีชื่อ * Form และวางไว้ในโครงการ UI ในโฟลเดอร์ Models
ในโครงการMvcPressพวกเขาอยู่ในโครงการ Data ซึ่งยังมีรหัสทั้งหมดเพื่อทำงานกับฐานข้อมูลและอีกเล็กน้อย (แต่ฉันไม่ได้แนะนำวิธีนี้มันเป็นเพียงตัวอย่าง)
ดังนั้นคุณจะเห็นว่ามีหลายมุมมอง ฉันมักจะเก็บ ViewModels ของฉัน (วัตถุ DTO) ในโครงการไซต์ แต่เมื่อฉันมีมากกว่า 10 รุ่นฉันชอบที่จะย้ายพวกเขาเพื่อแยกการชุมนุม โดยปกติในกรณีนี้ฉันจะย้ายตัวควบคุมเพื่อแยกการชุมนุมด้วย
คำถามอื่นคือวิธีการแมปข้อมูลทั้งหมดจากแบบจำลองไปยัง ViewModel ของคุณได้อย่างง่ายดาย ฉันขอแนะนำให้ดูที่AutoMapperไลบรารี่ ฉันชอบมันมากมันใช้งานได้ดีสำหรับฉัน
และฉันยังแนะนำให้ดูที่โครงการSharpAr Architecture มันให้สถาปัตยกรรมที่ดีมากสำหรับโครงการและมันมีกรอบและแนวทางที่ยอดเยี่ยมมากมายและชุมชนที่ยอดเยี่ยม


8
ViewModels! = DTO
Bart Calixto

6

นี่คือข้อมูลโค้ดจากแนวทางปฏิบัติที่ดีที่สุดของฉัน:

    public class UserController : Controller
    {
        private readonly IUserService userService;
        private readonly IBuilder<User, UserCreateInput> createBuilder;
        private readonly IBuilder<User, UserEditInput> editBuilder;

        public UserController(IUserService userService, IBuilder<User, UserCreateInput> createBuilder, IBuilder<User, UserEditInput> editBuilder)
        {
            this.userService = userService;
            this.editBuilder = editBuilder;
            this.createBuilder = createBuilder;
        }

        public ActionResult Index(int? page)
        {
            return View(userService.GetPage(page ?? 1, 5));
        }

        public ActionResult Create()
        {
            return View(createBuilder.BuildInput(new User()));
        }

        [HttpPost]
        public ActionResult Create(UserCreateInput input)
        {
            if (input.Roles == null) ModelState.AddModelError("roles", "selectati macar un rol");

            if (!ModelState.IsValid)
                return View(createBuilder.RebuildInput(input));

            userService.Create(createBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }

        public ActionResult Edit(long id)
        {
            return View(editBuilder.BuildInput(userService.GetFull(id)));
        }

        [HttpPost]
        public ActionResult Edit(UserEditInput input)
        {           
            if (!ModelState.IsValid)
                return View(editBuilder.RebuildInput(input));

            userService.Save(editBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }
}


4

โดยส่วนตัวฉันขอแนะนำว่า ViewModel เป็นอะไรนอกจากเรื่องเล็กน้อยแล้วใช้คลาสแยกต่างหาก

หากคุณมีรูปแบบการดูมากกว่าหนึ่งแบบฉันขอแนะนำให้ทำการแบ่งพาร์ทิชันเป็นอย่างน้อยในไดเรกทอรี ถ้าโมเดลมุมมองถูกแบ่งใช้ในภายหลังพื้นที่ชื่อโดยนัยในไดเรกทอรีทำให้ง่ายต่อการย้ายไปยังแอสเซมบลีใหม่


2

ในกรณีของเราเรามีโมเดลพร้อมตัวควบคุมในโครงการแยกต่างหากจากมุมมอง

ตามกฎทั่วไปเราได้พยายามย้ายและหลีกเลี่ยงสิ่งของ ViewData ["... "] ส่วนใหญ่ให้กับ ViewModel ดังนั้นเราจึงหลีกเลี่ยงการหล่อและสายเวทซึ่งเป็นสิ่งที่ดี

ViewModel และมีคุณสมบัติทั่วไปบางอย่างเช่นข้อมูลการแบ่งหน้าสำหรับรายการหรือข้อมูลส่วนหัวของหน้าเพื่อวาด breadcrumbs และหัวเรื่อง ในขณะนี้คลาสฐานมีข้อมูลมากเกินไปในความคิดของฉันและเราอาจแบ่งออกเป็นสามส่วนซึ่งเป็นข้อมูลพื้นฐานและจำเป็นที่สุดสำหรับ 99% ของหน้าเว็บในโมเดลมุมมองพื้นฐานแล้วแบบจำลองสำหรับรายการและแบบจำลอง สำหรับรูปแบบที่เก็บข้อมูลเฉพาะสำหรับสถานการณ์นั้นและสืบทอดจากฐานหนึ่ง

สุดท้ายเราใช้โมเดลการดูสำหรับแต่ละเอนทิตีเพื่อจัดการกับข้อมูลเฉพาะ


0

รหัสในคอนโทรลเลอร์:

    [HttpGet]
        public ActionResult EntryEdit(int? entryId)
        {
            ViewData["BodyClass"] = "page-entryEdit";
            EntryEditViewModel viewMode = new EntryEditViewModel(entryId);
            return View(viewMode);
        }

    [HttpPost]
    public ActionResult EntryEdit(Entry entry)
    {
        ViewData["BodyClass"] = "page-entryEdit";            

        #region save

        if (ModelState.IsValid)
        {
            if (EntryManager.Update(entry) == 1)
            {
                return RedirectToAction("EntryEditSuccess", "Dictionary");
            }
            else
            {
                return RedirectToAction("EntryEditFailed", "Dictionary");
            }
        }
        else
        {
            EntryEditViewModel viewModel = new EntryEditViewModel(entry);
            return View(viewModel);
        }

        #endregion
    }

รหัสในรูปแบบการดู:

public class EntryEditViewModel
    {
        #region Private Variables for Properties

        private Entry _entry = new Entry();
        private StatusList _statusList = new StatusList();        

        #endregion

        #region Public Properties

        public Entry Entry
        {
            get { return _entry; }
            set { _entry = value; }
        }

        public StatusList StatusList
        {
            get { return _statusList; }
        }

        #endregion

        #region constructor(s)

        /// <summary>
        /// for Get action
        /// </summary>
        /// <param name="entryId"></param>
        public EntryEditViewModel(int? entryId)
        {
            this.Entry = EntryManager.GetDetail(entryId.Value);                 
        }

        /// <summary>
        /// for Post action
        /// </summary>
        /// <param name="entry"></param>
        public EntryEditViewModel(Entry entry)
        {
            this.Entry = entry;
        }

        #endregion       
    }

โครงการ:

  • DevJet.Web (โครงการเว็บ ASP.NET MVC)

  • DevJet.Web.App.Dictionary (โครงการห้องสมุดแยกต่างหาก)

    ในโครงการนี้ฉันทำบางโฟลเดอร์เช่น: DAL, BLL, BO, VM (โฟลเดอร์สำหรับรุ่นที่มีมุมมอง)


สวัสดีคุณสามารถแบ่งปันโครงสร้างของชั้นเรียนได้อย่างไร
Dinis Cruz

0

สร้างคลาสพื้นฐานของมุมมองโมเดลซึ่งมีคุณสมบัติที่จำเป็นทั่วไปเช่นผลลัพธ์ของการดำเนินการและข้อมูลเชิงบริบทคุณยังสามารถใส่ข้อมูลผู้ใช้และบทบาทปัจจุบัน

class ViewModelBase 
{
  public bool HasError {get;set;} 
  public string ErrorMessage {get;set;}
  public List<string> UserRoles{get;set;}
}

ในคลาสตัวควบคุมพื้นฐานมีวิธีเช่น PopulateViewModelBase () วิธีนี้จะเติมข้อมูลบริบทและบทบาทผู้ใช้ HasError และ ErrorMessage ตั้งค่าคุณสมบัติเหล่านี้หากมีข้อยกเว้นขณะดึงข้อมูลจาก service / db ผูกคุณสมบัติเหล่านี้ในมุมมองเพื่อแสดงข้อผิดพลาด บทบาทผู้ใช้สามารถใช้เพื่อแสดงส่วนซ่อนอยู่ในมุมมองตามบทบาท

เมื่อต้องการเติมโมเดลมุมมองในการดำเนินการรับที่แตกต่างกันมันสามารถทำให้สอดคล้องกันโดยมีตัวควบคุมฐานด้วยวิธีนามธรรม FillModel

class BaseController :BaseController 
{
   public PopulateViewModelBase(ViewModelBase model) 
{
   //fill up common data. 
}
abstract ViewModelBase FillModel();
}

ในการควบคุม

class MyController :Controller 
{

 public ActionResult Index() 
{
   return View(FillModel()); 
}

ViewModelBase FillModel() 
{ 
    ViewModelBase  model=;
    string currentAction = HttpContext.Current.Request.RequestContext.RouteData.Values["action"].ToString(); 
 try 
{ 
   switch(currentAction) 
{  
   case "Index": 
   model= GetCustomerData(); 
   break;
   // fill model logic for other actions 
}
}
catch(Exception ex) 
{
   model.HasError=true;
   model.ErrorMessage=ex.Message;
}
//fill common properties 
base.PopulateViewModelBase(model);
return model;
}
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.