views:

207

answers:

3

This is mostly for educational purposes. I'm trying to create the InputMapper class, which is used in this example:

var mapper = new InputMapper<SomeType>();
mapper.Map("some user input", someType => someType.IntProperty, "Input was not an integer");
mapper.Map("some user input", someType => someType.BoolProperty, "Input was not a bool");

SomeType someTypeInstance = mapper.CreateInstance();

My InputMapper class holds a collection of all of the mappings created using the Map() method. CreateInstance() will loop through the mappings trying to convert the user input and assign it to the property used in the lambda expression. As it loops through, it will save a collection of any FormatExceptions that are thrown.

My questions are:

  • In the InputMapper.Map() method, what should the lambda parameter type be?
  • In the InputMapper.CreateInstance() method, how do I go about trying to set the properties on the instance of T that I have created?

Thanks!

Update

Dr. Skeet asked for some more information about my intentions.

The InputMapper class will be used to assign user input to the members of any object, taking care of the conversion of the user input to the properties type. The interface of the class can be inferred from the example above.

Update 2

After some hand holding, Jon and Dan, got me there. Can you suggest improvements? Here's what I have: http://pastebin.com/RaYG5n2h

+3  A: 

For your first question, the Map method should probably be generic. For example:

public class InputMapper<TSource> where TSource : new()
{
    public void Map<TResult>(string input,
                             Expression<Func<TSource, TResult>> projection,
                             string text)
    {
        ...
    }
}

Now it's worth noting that your lambda expressions represent the property getters, but presumably you want to call the property setters. That means your code won't be compile-time safe - there could be a mapped property which is read-only, for example. Also, there's nothing to restrict the lambda expression to only refer to a property. It could do anything. You'd have to put in execution-time guards against that.

Once you have found the property setters though, you just need to create an instance of TSource using new TSource() (note the constructor constraint on TSource above) and then perform the appropriate conversions and call the property setters.

Without more details on what you're trying to do, I'm afraid it's not terribly easy to give a more detailed answer.

EDIT: The code to work out the property would look something like this:

var memberExpression = projection.Body as MemberExpression;
if (memberExpression == null)
{
    throw new ArgumentException("Lambda was not a member access");
}
var propertyInfo = memberExpression.Member as PropertyInfo;
if (propertyInfo == null)
{
    throw new ArgumentException("Lambda was not a property access");
}
if (projection.Parameters.Count != 1 ||
    projection.Parameters[0] != memberExpression.Expression)
{
    throw new ArgumentException("Property was not invoked on parameter");
}
if (!propertyInfo.CanWrite)
{
    throw new ArgumentException("Property is read-only");
}
// Now we've got a PropertyInfo which we should be able to write to - 
// although the setter may be private. (Add more tests for that...)
// Stash propertyInfo appropriately, and use PropertyInfo.SetValue when you 
// need to.
Jon Skeet
I have provided some more information in my question and I am looking for example code of how to assign a value to the property that the projection param describes.
Ronnie Overby
@Ronnie: I've edited the code - see if that helps you.
Jon Skeet
Thanks, I'll give it a shot.
Ronnie Overby
Getting "Error 3 The type or namespace name 'TResult' could not be found (are you missing a using directive or an assembly reference?)" when trying to compile. Same for TSource.
Ronnie Overby
@Ronnie: I suspect you haven't declared the method as I have then - `TResult` and `TSource` are type parameters; `TResult` is the type parameter for the method, and `TSource` is the type parameter for the type.
Jon Skeet
You're right. I'm almost there. Just getting the error for TResult now. Here's my code: http://pastebin.com/d3khznP3
Ronnie Overby
@Ronnie: You can't have a `List<InputMapping<TProperty>>` as a member field because there's no consistent `TProperty` (different properties will be of different types). You'll have to make `InputMapping<TProperty>` inherit from some non-generic base and make your field a `List<InputMapping>` (or something similar).
Dan Tao
@Ronnie: Do the extraction in Map, and change your InputMapping class to have a PropertyInfo instead of the projection.
Jon Skeet
@Dan - Sorry but I'm just not getting it, yet. Can you show me what you mean? Current: http://pastebin.com/MDpFsXHM
Ronnie Overby
@Jon - Thanks I'll try it.
Ronnie Overby
@Jon - Got it working. Thanks for baby sitting me.
Ronnie Overby
+1  A: 

Update: See here for a working example of what I think you're trying to do.

(Update 2: Looks like you already figured it out shortly before I posted this. Good! I'll leave it there for reference, anyway, even though it's not a very efficient implementation.)

Below is a demo program.

class CustomType
{
    public int Integer { get; set; }
    public double Double { get; set; }
    public bool Boolean { get; set; }
    public string String { get; set; }

    public override string ToString()
    {
        return string.Format("int: {0}, double: {1}, bool: {2}, string: {3}", Integer, Double, Boolean, String);
    }
}

class Program
{
    public static void Main(string[] args)
    {
        var mapper = new InputMapper<CustomType>();

        mapper.Map("10", x => x.Integer, "Unable to set Integer property.");
        mapper.Map("32.5", x => x.Double, "Unabled to set Double property.");
        mapper.Map("True", x => x.Boolean, "Unable to set Boolean property.");
        mapper.Map("Hello world!", x => x.String, "Unable to set String property.");

        var customObject = mapper.Create();

        Console.WriteLine(customObject);

        Console.ReadKey();
    }
}

Output:

int: 10, double: 32.5, bool: True, string: Hello world!

It sounds like you want to define your Map function like this:

class InputMapper<T>
{
    public void Map<TProperty>(string input,
                               Expression<Func<T, TProperty>> propertyExpression,
                               string errorMessage);
}

Then presumably what you want to do is work out from propertyExpression which property you want set based on the user input. Is that right?

I'm not entirely clear on why you wouldn't just want to define it like this, though:

class InputMapper<T>
{
    public void Map<TProperty>(string input,
                               Action<TProperty> propertySetter,
                               string errorMessage);
}

Then the usage would look something like:

mapper.Map<int>(
    "some user input",
    value => someType.IntProperty = value,
    "Input was not an integer"
);

(Note that your Map<TProperty> function would internally have to handle the matter of parsing the user input to the appropriate type, probably using something simple like Convert.ChangeType.)

Dan Tao
It looks like your suggestion to accept an action parameter puts the calling code in charge of assigning the input to the property. I don't want to do that. That's InputMapper's repsonsibility. I just want to tell the class the type I want it to create, and what user input to try to use to set the properties, along with an error message that will later be displayed to the user. The error message part is extraneous to the problem I really have and I shouldn't have included it in my question.
Ronnie Overby
@Ronnie: OK, fair enough; then go with my first suggestion. You can certainly go from an `Expression<Func<T, TProperty>>` to the property setter; but as Jon points out in his answer, you have to first decide what you want to do if calling code hands you something other than a property expression, as well as what to do when it turns out the property passed to you cannot be set. Just some things to be aware of.
Dan Tao
@Dan - Thanks a lot. Wish I could mark 2 answers as accepted. Jon got me there just a bit quicker.
Ronnie Overby
+1  A: 
public void Map<TValue>(string input, Expression<Func<T, TValue>> property, string message);

Then CreateInstance could look like:

public T CreateInstance() 
{
    T result = new T();
    foreach (var lambda in map) 
    {
        ((PropertyInfo)((MemberExpression)lambda.Body).Member).SetValue(result, null, propertyValueForLambdaThatYouStored);  
    }
    return result;
}

You could add some checks to throw better exceptions if the expression in the lambda isn't a property reference.

Kirk Woll
Thank! I'll give this a shot. I'm happy that someone understood me.
Ronnie Overby