2

I have a scenario that is giving me a headache, and I thought I was going in the right direction, now I have found another problem. It's complicated so, Let's start at the database:

I have a table with a key, and a value, a typeId and a few other properties. All I'm concerned with are the key and value essentially. My viewmodel contains a list of the Domain Object I mapped from the Data Object nhibernate uses in the repository layer. Here it is:

public class DomainModel
{
    public string Key { get; set; }
    public string Value { get; set; }
    public TypeEnum Type { get; set; }
}

**Note, the TypeEnum has nothing to do with the strong type of the Value for the Key. It is just a different way of categorizing the Keys/Values so that I can pull them from the database by that type.

Simple. Here is my viewmodel:

public class ViewModel
{
    public List<DomainModel> Models { get; set; }
}

The problem I have is that the values for the keys are different data types. Sometimes they are booleans and I want an Html.CheckBoxFor(model => model.Value), and sometimes they are strings and an Html.TextBoxFor(model => model.Value) will suffice.

Here is my razor view:

@foreach (var setting in Model.Models)
{
    <tr>
        <td>@Html.Label(setting.Key)</td>
        <td>@Html.TextBox(setting.Value)</td>
    </tr>
}

What is the best area of the application to slice here, and do some type inspection or something so that I have the appropriate Html Elements on the page? How would I even go about that? Am I missing something that is really obvious and simple here? Additionally, how do I go about getting a Display Name Attribute for the Keys, based on the Key's value? They are just PacalCasedBunchedDescriptionNames at the moment. Am I just way off on design here or what?

BeeTee2
  • 777
  • 4
  • 13
  • 2
    You will need some **metadata**. These should be driven by the `TypeEnum` switch. You can/should create some Provider pattern implementation available anywhere, providing read-only access to these metadata. Having this, we could use the metadata e.g. on BL for Validation, and MVC Views could drive rendering based on these settings... *(There is a small overview about NHibernate dynamic world http://stackoverflow.com/a/16692541/1679310)* – Radim Köhler Jan 01 '14 at 09:22

2 Answers2

1

What I ended up doing was just leaving the view and viewmodel flat, rather than trying to do some dynamic type implication there. This also allowed me to keep my validation and other attributes nice and neat.

Domain Model is still the same, I actually ended up removing Type, and creating a SaveDomainModel in my Business Layer since every time I save / update a collection, it is only for 1 type of Setting:

public class SaveDomainModel
{
    public List<DomainModel> DomainModels { get; set; }
    public SettingTypeEnum SettingType { get; set; }
}

I changed my DomainModel to:

public class DomainModel
{
    [Required]
    public string Key { get; set; }
    [Required]
    public string Value { get; set; }
}

And flattened out my ViewModel:

public class EditViewModel
{
    [DisplayName("Display Name:")]
    [Required]
    public int AnIntProp { get; set; }

    [DisplayName("Another Display Name:")]
    [Required]
    public string HereIsAString { get; set; }

    [DisplayName("Bool Display:")]
    [Required]
    public bool ImABool{ get; set; }
}

So now my controller looks like this for the POST:

[HttpPost]
public virtual ActionResult Edit(EditViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        SaveSettings(viewModel);
        return RedirectToAction(MVC.Settings.Edit());
    }
    return View(viewModel);
}

private void SaveSettings(EditViewModel viewModel)
{
    var settings = MapEditViewModelToDomainModels(viewModel);
    var saveDomainModel = new SaveDomainModel
    {
        DomainModels = settings,
        SettingType = SettingTypeEnum.Application
    };
    _settingsService.SaveSettings(saveDomainModel);
}

This is really the missing link that I had not came upon before which I stumbled upon in this post: Enumerating through an object's properties (string) in C#

Then to map from flat view model to Domain obj I used that Map... function in SaveSettings().

private static List<DomainModel> MapEditViewModelToDomainModels(EditViewModel viewModel)
{
    var settings = new List<DomainModel>();

    var stringPropertyNamesAndValues = viewModel.GetType().GetProperties().Where(p => p.CanRead).Select(p => new {Name = p.Name, Value = p.GetValue(viewModel, null)});
    foreach (var pair in stringPropertyNamesAndValues)
    {
        var model= new DomainModel
        {
            Key = pair.Name,
            Value = pair.Value.ToString()
        };
        settings.Add(model);
    }

    return settings;
}

So then I was able to keep my view flat like so:

<tr>
    <td>@Html.LabelFor(model => model.SomeString)</td>
    <td>@Html.TextBoxFor(model => model.SomeString)</td>
</tr>
<tr>
    <td>@Html.LabelFor(model => model.SomeBoolean)</td>
    <td>@Html.CheckBoxFor(model => model.SomeBoolean)</td>
</tr>
...>

Then to finish it off I added an UpdateCollection() to my Repository, which is called in the service layer, after mapping from DomainObj -> DataObj, obviously.

public void SaveSettings(SaveDomainModel model)
{
    var settings = MapDomainModelToList(model).AsQueryable();
    _repository.UpdateCollection(settings);
}
private IEnumerable<DataObj> MapDomainModelToList(SaveDomainModel saveDomainModel)
{
    var settings = new List<Setting>();

    foreach (var domainModel in saveDomainModel.DomainModels)
    {
        var setting = GetSetting(domainModel.Key, saveDomainModel.SettingType);

        if (!String.Equals(setting.Value, domainModel.Value, StringComparison.CurrentCultureIgnoreCase))
        {
            setting.Value = domainModel.Value;
            setting.LastUpdated = DateTime.Now;
            settings.Add(setting);
        }
    }

    return settings;
}
public bool UpdateCollection(IQueryable<T> entities)
{
    using (var transaction = _session.BeginTransaction())
    {
        foreach (var entity in entities)
        {
            _session.Update(entity);
        }

        transaction.Commit();
    }
    return true;
}
Community
  • 1
  • 1
BeeTee2
  • 777
  • 4
  • 13
0

One way would be to just use the MVC features to implement this and not touch nhibernate specifics at all. I mean, nhibernate is just your data layer right? This should not influence your presentation layer at all.

And you have already a TypeEnum property on your model. I guess this will define if the property should be displayed as checkbox, textbox or whatever... If so, write a custom editortemplate for your DomainModel type and have the logic in one place of how to present an instance of DomainModel.

If you are curious about what the editor templates are in MVC, have a look into Scott's blog or this one

To give you an example of how this could look like:

The models:

public class Entity : List<Property>
{
    public Entity()
    {
    }
}

public class Property
{
    public string Name { get; set; }
    public string Value { get; set; }
    public DisplayType DisplayType { get; set; }
}

public enum DisplayType
{
    TextBox,
    Checkbox
}

A Controller/Action for testing:

    public ActionResult Index()
    {
        var entity = new Entity();
        entity.Add(new Property()
        {
            DisplayType = DisplayType.Checkbox,
            Name = "Check1",
            Value = "True"
        });

        entity.Add(new Property()
        {
            DisplayType = DisplayType.Checkbox,
            Name = "Check2",
            Value = "False"
        });

        entity.Add(new Property()
        {
            DisplayType = DisplayType.TextBox,
            Name = "Input1",
            Value = ""
        });

        //ViewBag.Entity = entity;
        return View(entity);
    }

The View could look like this:

@using WebApplication6.Models
@model WebApplication6.Models.Entity

@{
    ViewBag.Title = "Edit Entity";
}

<h2>Edit Entity</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Entity</h4>
        <hr />
        @Html.ValidationSummary(true)
        <div class="form-group">
            @for (var i = 0; i < Model.Count;i++ )
            {
                <div class="form-group">
                    @Html.EditorFor(m => m[i], "PropertyEditor")
                </div>
            }
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}

All the magic is now hidden within @Html.EditorFor(m => m[i], "PropertyEditor") Create a folder EditorTemplates under View/Shared and add a file for your template e.g. PropertyEditor.cshtml

The template could look like this:

@model WebApplication6.Models.Property

@if (Model != null)
{
    <label for="@Model.Name" class="col-sm-2 control-label">@Model.Name</label>

    switch (Model.DisplayType)
    {
        case WebApplication6.Models.DisplayType.TextBox:
            <div class="col-sm-10">@Html.TextBox(Model.Name, Model.Value)</div>
            break;
        case WebApplication6.Models.DisplayType.Checkbox:
            <div class="col-sm-10">@Html.CheckBox(Model.Name, bool.Parse(Model.Value))</div>
            break;
    }
}
MichaC
  • 13,104
  • 2
  • 44
  • 56
  • Thank for taking the time to reply, however I should've clarified in my original post that the TypeEnum is unrelated to the strong .net type of the Value for the Key. It is simply an enum to allow me to pull the keys / values from the database for a particular type, as they are used in various areas of the application. But that is an idea. To add another int to the database, and map it to an enumeration in my application, and drive the view from that. I'm sure that would work, however that seems like it is a pretty strong violation to separation of concerns. Thoughts? – BeeTee2 Jan 02 '14 at 02:53