Strongly-Typed ASP.NET MVC with ADO.NET Entity Framework [closed]
Asked Answered
S

2

13

I've finally gotten this working after days of struggle.

I've got a simple database of People and Departments:

ADO.NET Entity Framework Entity Data Model diagram with Department and Person objects http://img39.imageshack.us/img39/1368/edmxdepartmentperson.gif

I can use strongly-typed ASP.NET MVC views for reference/navigation properties! See the list of departments...

ASP.NET MVC with DropDownList http://img11.imageshack.us/img11/7619/dropdownlistdepartment.gif

Part of my Person/Edit view:

<% using (Html.BeginForm()) {%>
    <%= Html.Hidden("Id", Model.Id) %>
    <fieldset>
        <legend>Fields</legend>
        <p>
            <label for="Name">Name:</label>
            <%= Html.TextBox("Name", Model.Name) %>
        </p>
        <p>
            <label for="DepartmentId">Department:</label>
            <%= Html.DropDownList("DepartmentId", new SelectList((IEnumerable)ViewData["Departments"], "Id", "Name"))%>
        </p>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
<% } %>

Part of my Person controller:

//
// GET: /Person/Edit/5

public ActionResult Edit(Guid id)
{
    ViewData["Departments"] = ctx.Department;
    Person model = (from Person p in ctx.Person
                    where p.Id == id
                    select p).FirstOrDefault();
    return View(model);
}

//
// POST: /Person/Edit

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Person model)
{
    ctx.AttachUpdated(model);  //extension
    ctx.SaveChanges();
    return RedirectToAction("Index");
}

To get this working, I extended the Person EntityObject with a new DepartmentId property.

using System;
using System.Data;
using System.Data.Objects.DataClasses;

namespace ProjectName.Models
{
    public partial class Person : EntityObject
    {
        public Guid DepartmentId
        {
            get
            {
                try
                {
                    return (Guid)this.DepartmentReference.EntityKey.EntityKeyValues[0].Value;
                }
                catch
                {
                    return Guid.Empty;
                }
            }
            set
            {
                this.DepartmentReference.EntityKey = new EntityKey("JunkEntities.Department", "Id", value);
            }
        }
    }
}

And I extended the Entity Framework ObjectContext with new AttachUpdated and ApplyReferencePropertyChanges methods:

using System;
using System.Data;
using System.Data.Objects;
using System.Data.Objects.DataClasses;

public static class EntityFrameworkExtensionMethods
{

    public static void AttachUpdated(this ObjectContext ctx, EntityObject objectDetached)
    {
        if (objectDetached.EntityKey == null)
        {
            String entitySetName = ctx.DefaultContainerName + "." + objectDetached.GetType().Name;
            Guid objectId = (Guid)objectDetached.GetType().GetProperty("Id").GetValue(objectDetached, null);
            objectDetached.EntityKey = new System.Data.EntityKey(entitySetName, "Id", objectId);
        }
        if (objectDetached.EntityState == EntityState.Detached)
        {
            object currentEntityInDb = null;
            if (ctx.TryGetObjectByKey(objectDetached.EntityKey, out currentEntityInDb))
            {
                ctx.ApplyPropertyChanges(objectDetached.EntityKey.EntitySetName, objectDetached);
                ctx.ApplyReferencePropertyChanges((IEntityWithRelationships)objectDetached,
                                                  (IEntityWithRelationships)currentEntityInDb);  //extension
            }
            else
            {
                throw new ObjectNotFoundException();
            }
        }
    }

    public static void ApplyReferencePropertyChanges(this ObjectContext ctx, IEntityWithRelationships newEntity, IEntityWithRelationships oldEntity)
    {
        foreach (var relatedEnd in oldEntity.RelationshipManager.GetAllRelatedEnds())
        {
            var oldRef = relatedEnd as EntityReference;
            if (oldRef != null)
            {
                var newRef = newEntity.RelationshipManager.GetRelatedEnd(oldRef.RelationshipName, oldRef.TargetRoleName) as EntityReference;
                oldRef.EntityKey = newRef.EntityKey;
            }
        }
    }

}

I just wanted to document my progress here. Please suggest improvements.


Thanks:

Subminiature answered 28/5, 2009 at 18:12 Comment(11)
Nice job, but unfortunately stackoverflow.com in not the place for you to document your progress. I voted to close: "not a real question".Thunderstorm
Don't you need to exclude the ID property when binding the person object here : public ActionResult Edit(Guid id, Person Model) ?Slipslop
Ah, I missed the part about "suggesting improvements". Let it live, I say.Thunderstorm
What if I type a bogus ID on here : public ActionResult Edit(Guid id) ? You're not checking if a person exists with that ID, and not displaying an error to user if it doesn't exists.Slipslop
cagdas, I've removed the id parameter from the Post Edit function.Subminiature
What I meant was something like this : public ActionResult Edit(Guid id, [Bind(Exclude = "Id")] Person Model) but if removing the ID parameter also works, then I guess it's OK. My prev comment was more like a question than suggestion.Slipslop
cagdas, I think edits work without the id parameter because I've got it as a hidden field. Is this a bad practice?Subminiature
I think it should work fine in this case. However you should at least know how the Bind attribute works so you can use it when you don't want a property to be updatable by the users. For example, if you had a CreationDate field in DB, then a malicious user could have added CreationDate=someBogusDate to the form collection (even though you didn't have that on your form) and have that automatically binded to your object. With [Bind(Exclude = "CreationDate")], you can avoid that.Slipslop
cagdas, I see what you mean. That's a good thing to know about. Thank you.Subminiature
Related question: #923900Subminiature
Just like to say that I'm glad this was left to 'live', and that stackoverflow should be flexible for people to document stuff like this. for a couple of reasons. 1). It ranks highly in search, letting other developers learn off it. i.e. the community benefits. 2) Supporting material like this can be used for other question answers.Fermium
M
8

I've begun working with ASP.NET MVC which is why I came upon this thread, so I'm not sure if you you're still checking for improvements.

I don't like the idea of adding the new property to a partial class on the entity framework because it doesn't allow for as much change. Try labeling your Deparment DropDown "Department.Id" like this

<p>
    <label for="Department.Id">Department:</label>
<%= Html.DropDownList("Department.Id", new SelectList((IEnumerable)ViewData["Departments"], "Id", "Name"))%>
</p>

The ModelBinding of the MVC Framework will pick up the value and apply it to the "Id" Property of the "Department" Navigation Property. What I found is that the other values of Department are null, but that is not significant. Now you have a way of retrieving the correct Department Entity and applying it to the Department Navigation Property of the new Person Entity created in the Model Bind to your Action parameter, something like:

newPerson.Department = ctx.Department.First(d => d.DepartmentId == newPerson.Department.Id);

In doing it this way, you don't need to update your Entity at all for a property it should have.

Monosome answered 22/9, 2009 at 2:22 Comment(1)
good and clean! Zack, you could also set EntityKey on your controllers Edit method: newPerson.DepartmentReference.EntityKey = new EntityKey("YourEntities.Department","DepartmentId", int.Parse(Request.Form["DepartmentId"]));Exhortative
S
0

Improve your Edit controlling so that it handles the exceptions that are thrown and redisplays the input the user has typed in so far. I'm sure you were about to ;)

Update your view to have validators:

<label for="Name">Name:</label>
<%= Html.TextBox("Name", Model.Name) %>
<%= Html.ValidationMessage("Name", "*") %>

and then utilize them in your editing:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Person Model)
{
    try
    {
       ctx.AttachUpdated(Model);  //extension
       ctx.SaveChanges();
       return RedirectToAction("Index");
    }
    catch
    {
        foreach (var err in Model.Errors)
          ModelState.AddModelError(err.PropertyName, err.ErrorMessage)

        return View(Model);
    }
}
Saprogenic answered 28/5, 2009 at 18:58 Comment(3)
"'....Person' does not contain a definition for 'Errors' and no extension method 'Errors' accepting a first argument of type '....Person' could be found (are you missing a using directive or an assembly reference?)"Subminiature
It depends on what framework you're using. Lightspeed and Linq2Sql give you the Errors property on each Entity. If you're building your Entities manually instead of using an ORM, then you'll need to build that property into your partial class for Person.Saprogenic
Something like Listing 3 and 4 in this article: asp.net/Learn/mvc/tutorial-16-cs.aspxSaprogenic

© 2022 - 2024 — McMap. All rights reserved.