Mapping a data object to a suitable ViewModel when the data type is unknown at build time

Background and Problem
I have a WPF application that basically consists of a set of different skill objects, each inheriting from an abstract base class (SkillBase). Each skill must have its own View and ViewModel to support editing the values โ€‹โ€‹of a particular skill. When the user selects a skill in the list and clicks Edit, the corresponding Edit-ViewModel should be created using the skill entered into it.

How do I choose which Edit-ViewModel should be created when I only have a Skill Data Object? Skills can be added via a plugin system, so I don't know the type of skills beforehand.

My solution today (which works) is to decorate each Edit-ViewModel with an attribute (EditViewModelForAttribute) indicating what datatype it should map. Is this the way to go or are there any better ways to do this? Am I even on the right track?

Other solutions I can think of is to omit the attribute and use some sort of Edit-ViewModels naming convention, and another solution on startup would be to register the skill type with the Edit-ViewModel type via injected service ( EditService.RegisterEditViewModel(typeof(WalkSkill), typeof(EditWalkSkillViewModel));

)

This is my code:

"Data layer"

public abstract class SkillBase
{
    // Common properties for all skills
}

public class WalkSkill : SkillBase
{
    // Some properties
}

public class RunSkill : SkillBase
{
    // Some properties
}

      

"View Level Model"

public abstract class EditSkillViewModel<T> : ViewModelBase where T : Skill
{
    public abstract T Skill { get; protected set; }
}

[EditViewModelFor(typeof(WalkSkill))] // Attribute telling that this viewmodel should be instantiated when we want to edit a WalkSkill object
public class EditWalkSkillViewModel : EditSkillViewModel<WalkSkill>
{
    public override WalkSkill Skill { get; protected set; }

    public EditWalkSkillViewModel(WalkSkill skill)
    {
        Skill = skill;
    }
}

[EditViewModelFor(typeof(RunSkill))] // Attribute telling that this viewmodel should be instantiated when we want to edit a RunSkill object
public class EditRunSkillViewModel : EditSkillViewModel<RunSkill>
{
    public override RunSkill Skill { get; protected set; }

    public EditRunSkillViewModel(RunSkill skill)
    {
        Skill = skill;
    }
}

      

To find the correct ViewModel for a specific skill, I have an extension method to search for types in the app domain that has an EditViewModelForAttribute with a specific skin type:

public static ViewModelBase GetEditSkillViewModel(this Skill skill)
{
    var viewModelType = (from assembly in AppDomain.CurrentDomain.GetAssemblies()
                         from type in assembly.GetTypes()
                         where
                             type.IsDefined(typeof(EditViewModelForAttribute)) &&
                             type.GetCustomAttribute<EditViewModelForAttribute>().SkillType == skill.GetType()
                         select type).SingleOrDefault();

    return viewModelType == null ? null : (ViewModelBase)Activator.CreateInstance(viewModelType, skill);
}

      

What is called like this:

var editViewModel = selectedSkill.GetEditSkillViewModel();

      

+3


source to share


3 answers


The current solution you are using seems fine to me if you only have one ViewModel per skill. However one suggestion, using FirstOrDefault

instead of SingleOrDefault

and cache the result somewhere. Searching for all types can be time consuming.



+1


source


When it comes to software development, you seem to be on the right track. There is nothing wrong with your current decision. This is the classic approach if you are using frameworks like MEF.

However, if you are going to continue; consider switching to MEF or any other framework instead of writing your own attributes / imports / other jargon.



// Define other methods and classes here
public abstract class EditSkillViewModel<T> where T : SkillBase
{
    public T Skill { get; protected set; }

    protected EditSkillViewModel(T skill){
        Skill = skill;
    }
}

public static class EditSkillViewModelManager
{
    private static IDictionary<Type, Type> _types;

    public static EditSkillViewModel<T> CreateEditSkillViewModel<T>(T skill) 
        where T : SkillBase
    {
        return (EditSkillViewModel<T>)Activator.CreateInstance(_types[typeof(T)], skill);
    }

    static EditSkillViewModelManager()
    {
        var editSkillViewModelTypes = from x in Assembly.GetAssembly(typeof(SkillBase)).GetTypes()
                    let y = x.BaseType
                    where   !x.IsAbstract && 
                            !x.IsInterface &&
                            y != null && 
                            y.IsGenericType &&
                            y.GetGenericTypeDefinition() == typeof(EditSkillViewModel<>)
                    select x;

        _types = editSkillViewModelTypes
            .Select(x => 
                new { 
                        To = x, // EditWalkSkillViewModel
                        From = x.BaseType.GetGenericArguments()[0] // WalkSkill
                    })
            .ToList()
            .ToDictionary(x => x.From, x => x.To);

        _types.Dump();
    }
}

public class EditWalkSkillViewModel : EditSkillViewModel<WalkSkill>
{
    public EditWalkSkillViewModel(WalkSkill skill) : base(skill)
    {
    }
}

public class EditRunSkillViewModel : EditSkillViewModel<RunSkill>
{
    public EditRunSkillViewModel(RunSkill skill): base(skill)
    {
    }
}

      

+1


source


I was solving a very similar problem and this is what I did - converted to your classes:

  • Skills as you have right now + add a / generic attribute that says "I need a configuration like X".
  • Configuration class for each skill (can be shared / inherited if "walk" and "run" have speed, etc.).
  • View for each configuration with the attribute / general expression "I can edit a configuration of type X"
  • One EditViewModel that has a property of the Configuration class. This virtual machine can also have properties that are common to all Skills: name, ID, included, ....

It is based on the same idea as your solution, but the abstraction is somewhere else. I did it this way because I had to decouple the UI architecture from my logic. The only thing I need to change if I want to move from WPF to something else is to create Views for all configurations. There is only one EditViewModel that integrates the WPF frontend with my application logic / models.

+1


source







All Articles