Home MVC Storefront

SubSonic MVC Addin Updated for Beta 1

I posted a couple of weeks back that I created a prototype Addin for Visual Studio that would scaffold your MVC site for you using Linq To Sql as the data access bits. If you didn't get a chance to watch the screencast - have a look.

Last week I updated the bits to Beta 1 and also fixed some things - namely:

  • Visual Studio no longer crashes :)
  • You now have a choice as to what you want created
  • If there's a problem - you're told about it (and then you can tell me!)
  • Works if you have your Controllers/Views in the App folder (where I like to put em)
  • A bunch of other bug fixes

There's a lot more that needs to be done, but I want to build this thing with your feedback so do let me know what you think.

Some Detail (Actually, TONS of Detail)
The concept of the "scaffold" comes from Rails and it's the idea that outputting some basic CRUD pages will help you get started on creating your site. You can jam in some test data and then begin changing the pages as you need to; locking down the Edit/New pages for admins only etc - it's up to you.

The Addin is designed to work with the project structure as set forth by the MVC Beta 1 template - in this case I've created on from scratch called:

app

The next thing you need is a Linq To Sql DBML class. The scaffold bits need this file so it knows what your data access bits look like. This is an important part of the equation here - the Addin doesn't look at your database, it uses your Linq To Sql definition file.

Yes, when SubSonic 3.0 gets a bit closer I'll build make sure to offer that as a choice, and yes I will (eventually) make this work with EF.

The next step, then, is to add some Linq To Sql:

Northwind

... and as always I'm using Northwind. One day I'll change that addiction...

Once the DBML file is added (I'm using all the tables in Northwind - you don't have to do this... use whatever you like) I then right-click on a folder in my solution, or on the Project itself:

addin

 

This brings up the Scaffold menu. You can choose the things you want SubSonic to generate for you as well (note that SubSonic will never overwrite anything):

Scaffold

Select your table and you're good to go! In this case, contrary to what the image says, I selected "Products" and clicked "go" - this is what's created:

whatscreated

This picture is a bit big - sorry about that - but I wanted to make sure you know what get's added to your project:

  • An "App" folder. This is where your business logic (what I call "Services") goes, as well as your Repository classes, for abstracting your database calls (I'll talk about that in a second).
  • A Controller, with no notion of your data access strategy. It also is set up to be injected with your service class (this is for testability).
  • All the scripts that you'll need for the scaffold. I decided to put them in a "scaffold" folder for clarity - but you can change this as needed (I'll tell you how below).
  • Finally - all the Views that you need. I've deviated a bit from the "magic 7" that Rails uses in that I have both a List and Index Views - Rails only has Index. I've just found this to be more helpful - but I'd love to hear your opinion on it.

The Code
I've tried to make sure that what gets generated follows really simple, testable "conventions". These conventions are:

  • Programming against interfaces
  • Using the Repository Pattern for Data Access
  • Creating injectable Constructors

The Repository

To see this in action, let's take a look at the Data Access bits created for the Product class. The first thing that is created is the Interface:

namespace MvcApplication6.Data
{
  
    /// <summary>
    /// And interface for mocking the Product Repository
    /// </summary>
    public interface IProductRepository
    {
        void Delete(Product item);
        void Delete(int itemID);
        
        IQueryable<Product> GetProducts();
        
        IQueryable<Order_Detail> GetOrder_Details();
        IQueryable<Category> GetCategories();
        IQueryable<Supplier> GetSuppliers();

        
        void Save(Product item);
    }

}

Notice here that I'm using IQueryable<Product> as I do in the MVC Storefront. I've become a big fan of this pattern for its simplicity. Also notice that the namespace is set to "[APPLICATION].Data".

Next up - the Sql implementation, which uses Linq To Sql:

    /// <summary>
    /// A Linq To Sql Repository implementation
    /// </summary>
    public class SqlProductRepository : IProductRepository
    {
        NorthwindDataContext _db;
        public SqlProductRepository()
        {
            _db = new NorthwindDataContext();
        }

        #region Foreign Keys


        public IQueryable<Order_Detail> GetOrder_Details()
        {
            return from items in _db.Order_Details
                   select items;
        }
                
        public IQueryable<Category> GetCategories()
        {
            return from items in _db.Categories
                   select items;
        }
                
        public IQueryable<Supplier> GetSuppliers()
        {
            return from items in _db.Suppliers
                   select items;
        }
                
        
        #endregion

        /// <summary>
        /// Returns all Products, which you can then requery.
        /// Note: this is an IQueryable list - it's not executed
        /// until you enumerate it.
        /// </summary>
        public IQueryable<Product> GetProducts()
        {
            return from items in _db.Products
                   select items;
        }

        /// <summary>
        /// Saves the Product to the Database.
        /// </summary>
        public void Save(Product item)
        {
            //is this new or existing?
            var existingItem = (from x in _db.Products
                                where x.ProductID == item.ProductID
                                select x).SingleOrDefault();

            if (existingItem == null)
            {
                _db.Products.InsertOnSubmit(item);
            }
            
            _db.SubmitChanges();
        }

        /// <summary>
        /// Deletes the Product from the Database
        /// </summary>
        /// <param name="item"></param>
        public void Delete(Product item)
        {
            _db.Products.DeleteOnSubmit(item);
            _db.SubmitChanges();
        }

        /// <summary>
        /// Deletes the Product from the Database, using the passed-in ID
        /// </summary>
        public void Delete(int itemID)
        {
            _db.Products.DeleteAllOnSubmit(from x in _db.Products
                                          where x.ProductID == itemID
                                          select x);
            _db.SubmitChanges();
        }

Not much to add here - it's about as simple as you can get. Simplicity is my goal here as I don't want to get in your way!

The Services
The Business Logic is captured in the ProductService class, which builds on top of the IProductRepository. Notice the injectable constructor for testing purposes:

    /// <summary>
    /// A class for holding Business Logic for Products
    /// </summary>
    public class ProductService:IProductService
    {
        
        IProductRepository _ProductRepository;
        
        /// <summary>
        /// Injectable constructor
        /// </summary>
        public ProductService(IProductRepository ProductRepository)
        {
            _ProductRepository = ProductRepository;
        }

        /// <summary>
        /// Gets a Product By ID
        /// </summary>
        /// <param name="id"></param>
        public Product GetProduct(int id)
        {
           return (from items in _ProductRepository.GetProducts()
                  where items.ProductID==id
                  select items).SingleOrDefault();
        }

        /// <summary>
        /// Returns all Products
        /// </summary>
        public IList<Product> GetProducts()
        {
            return _ProductRepository.GetProducts().ToList();
        }

        /// <summary>
        /// Executes a paged query using server-side paging
        /// </summary>
        /// <param name="pageIndex">Zero-based index of the current page</param>
        /// <param name="pageSize">Size of all pages</param>
        /// <returns>PagedList</returns>
        public PagedList<Product> GetProducts(int pageIndex, int pageSize)
        {
            
            int skipRecords = pageIndex * pageSize;
            var qry = from items in _ProductRepository.GetProducts()
                      select items;

            return new PagedList<Product>(qry, pageIndex, pageSize);
    
        }

        /// <summary>
        /// Searches for Products by ProductName that start with the passed value
        /// </summary>
        public IList<Product> Search(string query)
        {
            return (from items in _ProductRepository.GetProducts()
                    where items.ProductName.StartsWith(query)
                   select items).ToList();
        }


        /// <summary>
        /// Returns all Order_Details
        /// </summary>
        public IList<Order_Detail> GetOrder_Details()
        {
            return _ProductRepository.GetOrder_Details().ToList();
        }
                
        /// <summary>
        /// Returns all Categories
        /// </summary>
        public IList<Category> GetCategories()
        {
            return _ProductRepository.GetCategories().ToList();
        }
                
        /// <summary>
        /// Returns all Suppliers
        /// </summary>
        public IList<Supplier> GetSuppliers()
        {
            return _ProductRepository.GetSuppliers().ToList();
        }
                

        /// <summary>
        /// Saves the item to the Repository
        /// </summary>
        /// <param name="item"></param>
        public void Save(Product item)
        {

            //validations go here...
            _ProductRepository.Save(item);
        }

        /// <summary>
        /// Deletes the item from the Repository
        /// </summary>
        /// <param name="id"></param>
        public void Delete(int id)
        {
            //validations go here...
            _ProductRepository.Delete(id);
        }
    }

The Controller
These things can then be passed into the Controller (or you can inject them yourself when testing):

    [HandleError]
    public class ProductController : Controller
    {

        IProductService _ProductService;

        /// <summary>
        /// Default constructor - creates SqlRepository
        /// </summary>
        public ProductController():this(new ProductService(new SqlProductRepository())) {}

        /// <summary>
        /// Constructor overload for testing
        /// </summary>
        public ProductController(IProductService ProductService)
        {
            _ProductService = ProductService;
            ViewData["ClassName"] = "Product";
            ViewData["Table"] = "Products";
        }

        #region Utility Methods

        /// <summary>
        /// Creates SelectLists for Foreign-keys
        /// </summary>
        void LoadForeignKeyLists(Product item)
        {
            var order_details = _ProductService.GetOrder_Details();
        ViewData["ProductID"] = new SelectList(order_details, "ProductID", "ProductID", item.ProductID);

        var categories = _ProductService.GetCategories();
        ViewData["CategoryID"] = new SelectList(categories, "CategoryID", "Description", item.CategoryID);

        var suppliers = _ProductService.GetSuppliers();
        ViewData["SupplierID"] = new SelectList(suppliers, "SupplierID", "HomePage", item.SupplierID);


        }

        #endregion

        /// <summary>
        /// Default view
        /// </summary>
        /// <returns></returns>
        public ActionResult Index()
        {
            return View();
        }


        /// <summary>
        /// Lists all Products
        /// </summary>
        public ActionResult List()
        {
            int pageSize = 20;
            int currentPage = 1;
            int totalPages = 1;
            int totalCount = 0;
            IList<Product> items;


            if (Request.Form["pg"] != null)
            {
                int.TryParse(Request.Form["pg"], out currentPage);
            }

            //search support
            if (Request.Form["q"] != null)
            {

                string search = Request["q"];
                items = _ProductService.Search(search);

            }
            else
            {
                items = _ProductService.GetProducts(currentPage - 1, pageSize);

                PagedList<Product> paged = (PagedList<Product>)items;
                totalCount = paged.TotalCount;
                totalPages = paged.TotalPages;

            }

            ViewData["TotalRecords"] = totalCount;
            ViewData["TotalPages"] = totalPages;
            ViewData["PageSize"] = pageSize;
            ViewData["CurrentPage"] = currentPage;
            
            return View(items);
        }

        /// <summary>
        /// Shows an individual Product
        /// </summary>
        public ActionResult Show(int id)
        {

            var item = _ProductService.GetProduct(id);
            return View(item);
        }

        /// <summary>
        /// Default form view for New
        /// </summary>
        [AcceptVerbs("GET")]
        //[Authorize(Roles="Administrator")]
        public ActionResult New()
        {

            var item = new Product();
            
            //initialize dates
            item.DateCreated = DateTime.Now;
            item.CreatedOn = DateTime.Now;
            item.ModifiedOn = DateTime.Now;


            LoadForeignKeyLists(item);
            return View(item);
        }

        /// <summary>
        /// Handles New post-back
        /// </summary>
        [AcceptVerbs("POST")]
        //[Authorize(Roles="Administrator")]
        public ActionResult New(FormCollection form)
        {

            var item = new Product();

            try {
                UpdateModel(item, new[] { 
                    "ProductID",
                    "ProductName",
                    "SupplierID",
                    "CategoryID",
                    "QuantityPerUnit",
                    "UnitPrice",
                    "UnitsInStock",
                    "UnitsOnOrder",
                    "ReorderLevel",
                    "Discontinued",
                    "DateCreated",
                    "ProductGUID",
                    "CreatedOn",
                    "CreatedBy",
                    "ModifiedOn",
                    "ModifiedBy",
                    "Deleted"                
                });

                _ProductService.Save(item);

                
                TempData["Message"] = item.ProductName + " Created";
                return RedirectToAction("New");

            } catch(Exception x) {
                TempData["ErrorMessage"] = "Oops! " + item.ProductName + " wasn't saved: "+x.Message;
                LoadForeignKeyLists(item);

                return View(item);
            }

        }


        /// <summary>
        /// Default form view for Edit
        /// </summary>
        [AcceptVerbs("GET")]
        //[Authorize(Roles="Administrator")]
        public ActionResult Edit(int id)
        {
            var item = _ProductService.GetProduct(id);
            LoadForeignKeyLists(item);


            return View(item);
        }

        /// <summary>
        /// Handles post-back for Edit
        /// </summary>
        [AcceptVerbs("POST")]
        //[Authorize(Roles="Administrator")]
        public ActionResult Edit(int id, FormCollection from)
        {

            var item = _ProductService.GetProduct(id);

            try {
                
                UpdateModel(item, new[] { 
                    "ProductID",
                    "ProductName",
                    "SupplierID",
                    "CategoryID",
                    "QuantityPerUnit",
                    "UnitPrice",
                    "UnitsInStock",
                    "UnitsOnOrder",
                    "ReorderLevel",
                    "Discontinued",
                    "DateCreated",
                    "ProductGUID",
                    "CreatedOn",
                    "CreatedBy",
                    "ModifiedOn",
                    "ModifiedBy",
                    "Deleted"                
                }); 
                
                _ProductService.Save(item);
                TempData["Message"] = item.ProductName + " Saved";
                return RedirectToAction("Edit");
            } catch(Exception x)
            {
                TempData["ErrorMessage"] = "Oops! " + item.ProductName + " wasn't saved - "+x.Message;
                LoadForeignKeyLists(item);
                
                return View(item);
            }
        }


        /// <summary>
        /// Handles post-back for Delete
        /// </summary>
        //[Authorize(Roles="Administrator")]
        public ActionResult Delete(int id)
        {

            var item = _ProductService.GetProduct(id);
            string itemName = item.ProductName;


            try {
                _ProductService.Delete(id);
                TempData["Message"] = itemName + " Deleted";
                return RedirectToAction("List");
            
            } catch (Exception x) {
                TempData["ErrorMessage"] = "Error deleting Product: " + x.Message;
                
                LoadForeignKeyLists(item);
                return View("Edit",item);
            }

        }
    }

Customization
When you install the Addin a directory will be created in C:\Program Files\SubSonic\SubSonic Makai and in there will be the binary bits that need to run as well as a folder called "Templates". These are simple token-replacement templates (for now) that you can modify as needed to fit your project.

I don't have a way to modify the "orchestration" aspect yet - in other words allowing you to customize the process or where things go (simply because I don't know how to do this). The only thing you can change right now is the code that's created.

To do this, create a directory in your project root called "_Templates" and put your custom template in there. The name of this template matters!

Installation
Ideally everything should "just work" - but I've had some issues with the installer not including required binaries (like SubSonic 2.1 of all things). I've tested and re-tested it so "it works on my machine" but... well that hasn't served me so well :).

The installer assumes you're running Vista (yah I know - I had to make a choice) and so tries to put the binaries into your Addin folder, which it thinks should be at "[PersonalFolder]\Visual Studio 2008\Addins" ("C:\Users\Rob\Visual Studio 2008\Addins" for example). Obviously, if you're running XP, that won't work for you.

If you're having ANY issues with the installer - be sure you grab the binaries installer and drop them into your Visual Studio 2008\Addins folder (you might have to create this folder manually).

Summary and Download
As you can see this thing works best when you're doing things from scratch - that's what it's designed to do. I still consider this a "prototype" and I would really love to get your feedback!

Download the Installer Here (MSI - just click and run)

Download the binaries Here (DLL's and Addin file - unzip into your Visual Studio\Addins folder, wherever it is)

blog comments powered by Disqus
Search Me
Subscribe

Index Of MVC Screencasts

You can watch all of the MVC Screencasts up at ASP.NET, and even leave comments if you like.

Popular Posts
 
My Tweets
  • @jasonbock Kittens are having a really, really rough go of it then - Oxite is #2 on Codeplex downloads: http://www.codeplex.com/
  • Ergodynamic urinals. Awesome. http://twitpic.com/102dz
  • @paulsterling went to Laurelwood yesterday - absolutely stellar
  • At Hopwerks in Portland; awesome beer, outstanding grub, and a play area for the girls. Portland rocks.
  • I seem to have found @shanselman's otter... With "carrot"and all
  About Me



Hi! My name is Rob Conery and I work at Microsoft. I am the Creator of SubSonic and was the Chief Architect of the Commerce Starter Kit (a free, Open Source eCommerce platform for .NET)

I live in Kauai, HI with my family, and when my clients aren't looking, I sometimes write things on my blog (giving away secrets of incalculable value).