2

I have lots of models with same basic structure in my MVC project. So, I created a master class like below.

    public class MasterTemplate
    {
        [Key]
        public int Id { get; set; }

        [Required]
        [StringLength(255)]
        public string Description { get; set; }
        public DateTime? UpdatedOn { get; set; }
        public string UpdatedBy { get; set; }
    }

And I created all my model classes like below.

    public class Industry : MasterTemplate
    {

    }

    public class Caste : MasterTemplate
    {

    }

    public class Gender : MasterTemplate
    {

    }

    public class Qualification : MasterTemplate
    {

    }

    public class BloodGroup: MasterTemplate
    {

    }

There are many more like this. Following is my code for IndustryController.

 public class IndustryController : Controller
    {
        private ApplicationDbContext _context { get; set; }
        private string UserId { get; set; }

        public IndustryController()
        {
            _context = new ApplicationDbContext();
            UserId = System.Web.HttpContext.Current.User.Identity.GetUserId();
        }


        public ActionResult Index(int id = 0)
        {
            Industry data = new Industry();
            if (id > 0)
                data = _context.Industries.SingleOrDefault(c => c.Id == id);
            if (data == null)
                data = new Industry();

            return View("Industry", data);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Save(Industry data)
        {
            if (!ModelState.IsValid)
                return View("Industry", data);

            var record = _context.Industries.Where(c => c.Description.Trim().ToLower() == data.Description.Trim().ToLower() && c.Id != data.Id);
            if (record.Count() > 0)
            {
                ModelState.AddModelError("Duplicate Industry", "Industry already exist");
                return View("Industry", data);
            }

            Industry cm = new Industry();
            if (data.Id >= 1)
            {
                cm = _context.Industries.SingleOrDefault(c => c.Id == data.Id);
                cm.Description = data.Description;
                cm.UpdatedOn = DateTime.Now;
                cm.UpdatedBy = UserId;
            }
            else
            {
                cm = data;
                _context.Industries.Add(cm);
            }
            _context.SaveChanges();


            return RedirectToAction("Index", new { id = 0 });

        }

And following is my code for IndustryView

@model Industry
@{
    ViewBag.Title = "Industries";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h3>Industries Management</h3>
<div class="row">
    <div class="col-md-4">
        @using (@Html.BeginForm("Save", "Industry"))
        {
            @Html.ValidationSummary("Please correct the following")
            @Html.HiddenFor(m => m.Id)

            <div class="form-group">
                <div>
                    @Html.LabelFor(m => m.Description)
                    @Html.TextBoxFor(m => m.Description, new { @class = "form-control", autocomplete = "off" })
                    @Html.ValidationMessageFor(m => m.Description)
                </div>
            </div>
            @Html.AntiForgeryToken()
            <button type="submit" class="btn btn-primary btn-sm">Save</button>
        }
    </div>
    <div class="col-md-8">
        <table class="table table-sm" id="mydata">
            <thead>
                <tr>
                    <th>
                        Industries
                    </th>
                    <th>

                    </th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
    </div>
</div>


@section scripts
{
    @Scripts.Render("~/bundles/jqueryval")
    <script>
        $(document).ready(function () {
            $("#mydata").DataTable({
                ajax: {
                    url: "/api/get/industries",
                    dataSrc: ""
                },
                columns: [
                    {
                        data: "description"
                    },
                    {
                        data: "id",
                        render: function (data) {
                            var url = '@Url.Action("Index", "Industry", new { id = "__data__" })';
                            return '<a href="' + url.replace('__data__', data) + '">Edit</a>';
                        }
                    }
                ]
            });
        });
    </script>
}

Now my problem is, code for controller and views for all the models in my project is almost similar. It is as above. So, I wanted to generalize them and create a single controller and view which can be used for all my other models. I am new to generics, tried the following code, but still not able to figure out the way forward. It is so confusing for me.

    public interface IMaster
    {
        int Id { get; set; }
        string Description { get; set; }
    }

 public class GenericController : Controller
    {
        private ApplicationDbContext _context { get; set; }
        private string UserId { get; set; }

        public GenericController()
        {
            _context = new ApplicationDbContext();
            UserId = System.Web.HttpContext.Current.User.Identity.GetUserId();
        }


        public ActionResult Index(int id = 0)
        {
            IMaster data = null;
            if (id > 0)
                data = _context.Industries.SingleOrDefault(c => c.Id == id);
            if (data == null)
                data = new Industry();

            return View("Generic", data);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Save(IMaster data)
        {
            if (!ModelState.IsValid)
                return View("Generic", data);

            var record = _context.Industries.Where(c => c.Description.Trim().ToLower() == data.Description.Trim().ToLower() && c.Id != data.Id);
            if (record.Count() > 0)
            {
                ModelState.AddModelError("Duplicate Industry", "Industry already exist");
                return View("Generic", data);
            }

            Industry cm = new Industry();
            if (data.Id >= 1)
            {
                cm = _context.Industries.SingleOrDefault(c => c.Id == data.Id);
                cm.Description = data.Description;
                cm.UpdatedOn = DateTime.Now;
                cm.UpdatedBy = UserId;
            }
            else
            {
                cm.Id = data.Id;
                cm.Description = data.Description;
                _context.Industries.Add(cm);
            }
            _context.SaveChanges();


            return RedirectToAction("Index", new { id = 0 });

        }
    }

Can somebody guide me in right direction, need to create a generic controller and view for all the similar models in my project.

3
  • 1
    Good question, I asked a very similar question a white ago, which might be helpful. Commented Apr 4, 2019 at 4:48
  • Wow, I think we both are following same patterns, I removed all repository, uniot of work pattern code from this question to keep it simple, look here for my other question, please check if you have an answer for this, will continue reading your other answer stackoverflow.com/questions/55350766/… Commented Apr 4, 2019 at 4:56
  • I'm not sure asp.net mvc lets us to create generic controller or custom generic views, but i think if you create generic controller or view you need to customize ioc manager too. Commented Apr 5, 2019 at 8:58

1 Answer 1

1
+50

I have not run it, but I am pretty confident, that this should do the trick! Actually the only truly generic part is the controller. The other stuff is just the usual polymorphism. And thank you for the inspiration. It was fun thinking about such a solution. Maybe I'll build something similar in the future.

A word of caution: You will bind the name of your controllers to the name of each Model. Just be aware of this! There is a naming schema that must be kept or you break it.

public class [ModelName]Controller : MasterController<ModelName>
{
}

The ajax endpoints will end with the value of [PluralName]
(Read on in the View to know, what I mean.)

You will need an additional Property in the MasterTemplate. Ideally make it abstract, so you won't forget to implement it in the derived classes. This is for the Plural Name in the View's header and the ajax call in the View.

public abstract class MasterTemplate
{
    [Key]
    public int Id { get; set; }

    public abstract string PluralName {get;}

    [Required]
    [StringLength(255)]
    public string Description { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public string UpdatedBy { get; set; }
}

Industry will then look like this

public class Industry: MasterTemplate
{
  public override string PluralName => "Industries"
}

Make a truly generic Controller and derive all other Controllers from it like

public class IndustryController : MasterController<Industry>
{
   //done everthing else is in the master :)
}

And here the generic MasterController<T>.

public class MasterController<T> : Controller where T : MasterTemplate, new()
{
    private ApplicationDbContext _context { get; set; }
    private string UserId { get; set; }

    public MasterController()
    {
        _context = new ApplicationDbContext();
        UserId = System.Web.HttpContext.Current.User.Identity.GetUserId();
    }


    public ActionResult Index(int id = 0)
    {
        T data = (id > 0) 
               ? data = _context.Set<T>().SingleOrDefault(c => c.Id == id) ?? new T() 
               : new T();

        return View("View", data);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Save(T data)
    {
        if (!ModelState.IsValid)
            return View("View", data);

        var record = _context.Set<T>().Where(c => c.Description.Trim().ToLowerInvariant() == data.Description.Trim().ToLowerInvariant() && c.Id != data.Id);
        if (record.Count() > 0)
        {
            ModelState.AddModelError($"Duplicate {typeof(T).Name}", $"{typeof(T).Name} already exist");
            return View("View", data);
        }

        if (data.Id >= 1)
        {
            T cm = _context.Set<T>().SingleOrDefault(c => c.Id == data.Id);
            cm.Description = data.Description;
            cm.UpdatedOn = DateTime.Now;
            cm.UpdatedBy = UserId;
        }
        else
        {
            _context.Set<T>().Add(data);
        }
        _context.SaveChanges();


        return RedirectToAction("Index", new { id = 0 });

    }

Name the View "View" (or just the same, as you call it in the MasterController) and place it in the Shared Folder, for every controller to find it there.

@model MasterTemplate
@{
    string name = Model.GetType().Name;
    ViewBag.Title = name;
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h3>@Model.PluralName Management</h3>
<div class="row">
    <div class="col-md-4">
        @using (@Html.BeginForm("Save", name))
        {
            @Html.ValidationSummary("Please correct the following")
            @Html.HiddenFor(m => m.Id)

            <div class="form-group">
                <div>
                    @Html.LabelFor(m => m.Description)
                    @Html.TextBoxFor(m => m.Description, new { @class = "form-control", autocomplete = "off" })
                    @Html.ValidationMessageFor(m => m.Description, $"{name} is required.", new { @class = "text-danger" })
                </div>
            </div>
            @Html.AntiForgeryToken()
            <button type="submit" class="btn btn-primary btn-sm">Save</button>
        }
    </div>
    <div class="col-md-8">
        <table class="table table-sm" id="mydata">
            <thead>
                <tr>
                    <th>
                        @(name)
                    </th>
                    <th>

                    </th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
    </div>
</div>


@section scripts
{
    @Scripts.Render("~/bundles/jqueryval")
    <script>
        $(document).ready(function () {
            $("#mydata").DataTable({
                ajax: {
                    url: "/api/get/@(Model.PluralName)",
                    dataSrc: ""
                },
                columns: [
                    {
                        data: "description"
                    },
                    {
                        data: "id",
                        render: function (data) {
                            var url = '@Url.Action("Index", "@(name)", new { id = "__data__" })';
                            return '<a href="' + url.replace('__data__', data) + '">Edit</a>';
                        }
                    }
                ]
            });
        });
    </script>
}
Sign up to request clarification or add additional context in comments.

9 Comments

I am not 100% sure, if @name is avaiable throughout the whole view. If not, replace "name" with "Model.GetType().Name".
Thank you so much for the answer, .GetTable<T>() is giving an error, should I import any missing namespace.
Replaced it with .Set<T>(), will let you know if it's working after building it completely
this is also pretty easy: tutorialsteacher.com/mvc/htmlhelper-validationmessagefor, I edited the post accordingly. I went for the ErrorMessage in the ValidationMessageFor() instead of the RequiredAttribute, because all you want, is to replace the name of the Item.
Btw.: I initially changed as little as necessary from your "Template" but you should consider using ToLowerInvariant() instead of ToLower(). I ran into some problems doing exactly that (But NOT SQL related). The Turkish lower case 'i' is a special unicode char and not our usual 'i'. But then again, it gets translated to SQL, which is case insensitive, so it should even work without the ToLower() on both sides. Worth testing at least.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.