14

This is related to a prior question of mine C# Generic List conversion to Class implementing List<T>

I have the following code:

public abstract class DataField
{
    public string Name { get; set; }
}

public class DataField<T> : DataField
{
    public T Value { get; set; }
}

public static List<DataField> ConvertXML(XMLDocument data) {  
     result = (from d in XDocument.Parse(data.OuterXML).Root.Decendendants()
                      select new DataField<string>
                      {
                          Name = d.Name.ToString(),
                          Value = d.Value
                      }).Cast<DataField>().ToList();  
    return result;
}

This works however I would like to be able to modify the select portion of the LINQ query to be something like this:

select new DataField<[type defined in attribute of XML Element]>
{
  Name = d.Name.ToString(),
  Value = d.Value
}

Is this just a poor approach? is it possible? Any suggestions?

Community
  • 1
  • 1
John Hartsock
  • 85,422
  • 23
  • 131
  • 146
  • 3
    I feel like you're never going to know what subclass of DataField you have anyways, so why not just use `class DataField{string name; object value;}` ? – cwharris Nov 17 '11 at 16:08
  • I dont really want to reference all values as object. If I was to take a similar approach, it would seem easier to just use class DataField{string name; string type; string value}. – John Hartsock Nov 17 '11 at 16:23
  • 1
    Sure, but then you'll have to parse your data each time before you use it. If you pre-parse the data into the objects, and then store them in the data field, you can just cast them when you're ready to use them. This is what ADO.NET does with DataReader (although DataReader delays execution of the parsing until the column values are needed). For instance, `if(myField.Value is DateTime { /* do that date thing */ }` or `if(myField.Name == "importantDateThing") { var date = (DateTime)myField.Value; /* do importanty date thing */ }` – cwharris Nov 17 '11 at 16:26
  • @xixonia....yes I understand that. But Casting like that feels wrong to me. I know the type and have the ability to construct a list of different types. – John Hartsock Nov 17 '11 at 16:29
  • Here's the thing though, you're STILL going to be casting. You're either checking the type of the DataField and casting, or you're checking the type of the object and casting. – cwharris Nov 17 '11 at 16:34
  • Basically my motivation is to place XML in a List where T can be a number of different structures. I know that simply using List works. I started out that way but it seems to me that it should be possible to define for each item in the list what T is and then use it without casting. – John Hartsock Nov 17 '11 at 16:35
  • So DataField is an abstract class with some useful abstract methods that *will* be implemented differently in subclasses? – cwharris Nov 17 '11 at 17:23
  • @xixonia....yes but I was trying to keep it simple in the example. – John Hartsock Nov 17 '11 at 17:30
  • What kind of operations are you going to do with the resulting `DataField`s? – svick Nov 20 '11 at 18:05
  • @svick ... Primarily Data Mapping between two similar objects in the same structure. – John Hartsock Nov 20 '11 at 22:51

11 Answers11

8

Here is a working solution: (You must specify fully qualified type names for your Type attribute otherwise you have to configure a mapping somehow...)

I used the dynamic keyword, you can use reflection to set the value instead if you do not have C# 4...

public static void Test()
{
    string xmlData = "<root><Name1 Type=\"System.String\">Value1</Name1><Name2 Type=\"System.Int32\">324</Name2></root>";

    List<DataField> dataFieldList = DataField.ConvertXML(xmlData);

    Debug.Assert(dataFieldList.Count == 2);
    Debug.Assert(dataFieldList[0].GetType() == typeof(DataField<string>));
    Debug.Assert(dataFieldList[1].GetType() == typeof(DataField<int>));
}

public abstract class DataField
{
    public string Name { get; set; }

    /// <summary>
    /// Instanciate a generic DataField<T> given an XElement
    /// </summary>
    public static DataField CreateDataField(XElement element)
    {
        //Determine the type of element we deal with
        string elementTypeName = element.Attribute("Type").Value;
        Type elementType = Type.GetType(elementTypeName);

        //Instanciate a new Generic element of type: DataField<T>
        dynamic dataField = Activator.CreateInstance(typeof(DataField<>).MakeGenericType(elementType));
        dataField.Name = element.Name.ToString();

        //Convert the inner value to the target element type
        dynamic value = Convert.ChangeType(element.Value, elementType);

        //Set the value into DataField
        dataField.Value = value;

        return dataField;
    }

    /// <summary>
    /// Take all the descendant of the root node and creates a DataField for each
    /// </summary>
    public static List<DataField> ConvertXML(string xmlData)
    {
        var result = (from d in XDocument.Parse(xmlData).Root.DescendantNodes().OfType<XElement>()
                      select CreateDataField(d)).ToList();

        return result;
    }
}

public class DataField<T> : DataField
{
    public T Value { get; set; }
}
Termit
  • 599
  • 4
  • 9
5

You cannot do this easily in C#. The generic type argument has to specified at compile time. You can use reflection to do otherwise

             int X = 1;
            Type listype = typeof(List<>);
            Type constructed = listype.MakeGenericType(  X.GetType()  );
            object runtimeList = Activator.CreateInstance(constructed);

Here we have just created a List<int>. You can do it with your type

parapura rajkumar
  • 24,045
  • 1
  • 55
  • 85
4

Different instances of a generic class are actually different classes.
I.e. DataField<string> and DataField<int> are not the same class at all(!)

This means, that you can not define the generic parameter during run-time, as it has to be determined during compile-time.

sehe
  • 374,641
  • 47
  • 450
  • 633
Svarog
  • 2,188
  • 15
  • 21
  • 1
    That's not completely right. You can create generic type at runtime using reflection. What you say is more like how C++ templates behave. – svick Nov 20 '11 at 18:04
  • In addition, DataField and DataField can still inherit from the same parent class, and therefore they can implement abstract methods to be called via a parent class reference. That was stated really badly, but you get the idea. – cwharris Nov 21 '11 at 17:01
4

I would say this is a poor approach. In reality, even after you parse your XML file, you're not going to know what types of "DataFields" you have. You might as well just parse them as objects.

However, if you know that you're only ever going to have x number of types, you can do like so:

var Dictionary<string, Func<string, string, DataField>> myFactoryMaps =
{
    {"Type1", (name, value) => { return new DataField<Type1>(name, Type1.Parse(value); } },
    {"Type2", (name, value) => { return new DataField<Type2>(name, Type2.Parse(value); }  },
};
cwharris
  • 17,835
  • 4
  • 44
  • 64
4

Termit's answer is certainly excellent. Here is a little variant.

     public abstract class DataField
        {
                public string Name { get; set; }
        }

        public class DataField<T> : DataField
        {
                public T Value { get; set; }
                public Type GenericType { get { return this.Value.GetType(); } }
        }

        static Func<XElement , DataField> dfSelector = new Func<XElement , DataField>( e =>
        {
                string strType = e.Attribute( "type" ).Value;
                //if you dont have an attribute type, you could call an extension method to figure out the type (with regex patterns)
                //that would only work for struct
                Type type = Type.GetType( strType );
                dynamic df = Activator.CreateInstance( typeof( DataField<>).MakeGenericType( type ) );

                df.Name = e.Attribute( "name" ).Value;
                dynamic value = Convert.ChangeType( e.Value , type );
                df.Value = value;
                return df;
        } );

        public static List<DataField> ConvertXML( string xmlstring )
        {
                var result = XDocument.Parse( xmlstring )
                                        .Root.Descendants("object")
                                        .Select( dfSelector )
                                        .ToList();
                return result;
        }


        static void Main( string[] args )
        {
                string xml = "<root><object name=\"im1\" type=\"System.String\">HelloWorld!</object><object name=\"im2\" type=\"System.Int32\">324</object></root>";

                List<DataField> dfs = ConvertXML( xml );
        }
Pierluc SS
  • 3,138
  • 7
  • 31
  • 44
3

you can create generic type by reflection

    var instance = Activator.CreateInstance( typeof(DataField)
                         .MakeGenericType(Type.GetType(typeNameFromAttribute) );
    // and here set properties also by reflection
wiero
  • 2,176
  • 1
  • 19
  • 28
2

@Termit and @Burnzy put forward good solutions involving factory methods.

The problem with that is that you're loading up your parsing routine with a bunch of extra logic (more testing, more errors) for dubious returns.

Another way to do it would be to use a simplified string-based DataField with typed read methods - the top answer for this question.

An implementation of a typed-value method that would be nice but only works for value types (which does not include strings but does include DateTimes):

public T? TypedValue<T>()
    where T : struct
{
    try { return (T?) Convert.ChangeType(this.Value, typeof(T)); }
    catch { return null; }
}


I'm assuming that you're wanting to use the type information to do things like dynamically assigning user-controls to the field, validation rules, correct SQL types for persistence etc.

I've done a lot of this sort of thing with approaches that seem a bit like yours.

At the end of the day you should seperate your metadata from your code - @Burnzy's answer chooses the code based on the metadata (a "type" attribute of the DataField element) and is a very simple example of this.

If you're dealing with XML, XSDs are a very useful and extensible form of metadata.

As far as what you store each field's data in - use strings because:

  • they are nullable
  • they can store partial values
  • they can store invalid values (makes telling the user to sort their act out more transparent)
  • they can store lists
  • special cases won't invade unrelated code because there aren't any
  • learn regular expressions, validate, be happy
  • you can convert them to stronger types really easily

I found it very rewarding to develop little frameworks like this - it is a learning experience and you'll come out understanding a lot more about UX and the reality of modelling from it.

There are four groups of test cases that I would advise you to tackle first:

  • Dates, Times, Timestamps (what I call DateTime), Periods (Timespan)
    • in particular, make sure you test having a different server locality from the client's.
  • lists - multi-select foreign keys etc
  • null values
  • invalid input - this generally involves retaining the original value

Using strings simplifies all this greatly because it allows you to clearly demarcate responsibilities within your framework. Think about doing fields containing lists in your generic model - it gets hairy rather quickly and it is easy to end up with a special case for lists in pretty much every method. With strings, the buck stops there.

Finally, if you want a solid implementation of this sort of stuff without having to do anything much, consider DataSets - old school I know - they do all sorts of wonderful things you wouldn't expect but you do have to RTFM.

The main downfall of that idea would be that it isn't compatible with WPF data binding - though my experience has been that reality isn't compatible with WPF data binding.

I hope I interpreted your intentions correctly - good luck either way :)

Community
  • 1
  • 1
Seth
  • 1,064
  • 11
  • 18
2

Unfortunately, there no inheritance relation between C<T> and C<string> for instance. However, you can inherit from a common non-generic class and in addition to this implement a generic interface. Here I use explicit interface implementation in order to be able to declare a Value property typed as object, as well as a more specifically typed Value property. The Values are read-only and can only be assigned through a typed constructor parameter. My construction is not perfect, but type safe and doesn't use reflection.

public interface IValue<T>
{
    T Value { get; }
}

public abstract class DataField
{
    public DataField(string name, object value)
    {
        Name = name;
        Value = value;
    }
    public string Name { get; private set; }
    public object Value { get; private set; }
}

public class StringDataField : DataField, IValue<string>
{
    public StringDataField(string name, string value)
        : base(name, value)
    {
    }

    string IValue<string>.Value
    {
        get { return (string)Value; }
    }
}

public class IntDataField : DataField, IValue<int>
{
    public IntDataField(string name, int value)
        : base(name, value)
    {
    }

    int IValue<int>.Value
    {
        get { return (int)Value; }
    }
}

The list can then be declared with the abstract base class DataField as generic parameter:

var list = new List<DataField>();
switch (fieldType) {
    case "string":
        list.Add(new StringDataField("Item", "Apple"));
        break;
    case "int":
        list.Add(new IntDataField("Count", 12));
        break;
}

Access the strongly typed field through the interface:

public void ProcessDataField(DataField field)
{
    var stringField = field as IValue<string>;
    if (stringField != null) {
        string s = stringField.Value;
    }
}
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
2

While the other questions mostly proposed an elegant solution to convert your XML elements to a generic class instance, I'm going to deal with the consequences of taking the approach to model the DataField class as a generic like DataField<[type defined in attribute of XML Element]>.

After selecting your DataField instance into the list you want to use these fields. Her polymorphism comes into play! You want to iterate your DataFields an treat them in a uniform way. Solutions that use generics often end up in a weird switch/if orgy since there is no easy way to associate behavior based on the generic type in c#.

You might have seen code like this (I'm trying to calculate the sum of all numeric DataField instances)

var list = new List<DataField>()
{
    new DataField<int>() {Name = "int", Value = 2},
    new DataField<string>() {Name = "string", Value = "stringValue"},
    new DataField<float>() {Name = "string", Value = 2f},
};

var sum = 0.0;

foreach (var dataField in list)
{
    if (dataField.GetType().IsGenericType)
    {
        if (dataField.GetType().GetGenericArguments()[0] == typeof(int))
        {
            sum += ((DataField<int>) dataField).Value;
        }
        else if (dataField.GetType().GetGenericArguments()[0] == typeof(float))
        {
            sum += ((DataField<float>)dataField).Value;
        }
        // ..
    }
}

This code is a complete mess!

Let's go try the polymorphic implementation with your generic type DataField and add some method Sum to it that accepts the old some and returns the (possibly modified) new sum:

public class DataField<T> : DataField 
{
    public T Value { get; set; }
    public override double Sum(double sum)
    {
       if (typeof(T) == typeof(int))
       {
           return sum + (int)Value; // Cannot really cast here!
       }
       else if (typeof(T) == typeof(float))
       {
           return sum + (float)Value; // Cannot really cast here!
       }
       // ...

       return sum;
    }
}

You can imagine that your iteration code gets a lot clearer now but you still have this weird switch/if statement in you code. And here comes the point: Generics do not help you here it's the wrong tool at the wrong place. Generics are designed in C# for giving you compile time type safety to avoid potential unsafe cast operations. They additionally add to code readability but that's not the case here :)

Let's take a look at the polymorphic solution:

public abstract class DataField
{
    public string Name { get; set; }
    public object Value { get; set; }
    public abstract double Sum(double sum);
}

public class IntDataField : DataField
{
    public override double Sum(double sum)
    {
        return (int)Value + sum;
    }
}

public class FloatDataField : DataField
{
    public override double Sum(double sum)
    {
        return (float)Value + sum;
    }
}

I guess you will not need too much fantasy to imagine how much adds to your code's readability/quality.

The last point is how to create instances of these classes. Simply by using some convention TypeName + "DataField" and Activator:

Activator.CreateInstance("assemblyName", typeName);

Short Version:

Generics is not the appropriate approach for your problem because it does not add value to the handling of DataField instances. With the polymorphic approach you can work easily with the instances of DataField!

saintedlama
  • 6,838
  • 1
  • 28
  • 46
1

It's not impossible as you can do this with reflection. But this isn't what generics were designed for and isn't how it should be done. If you're going to use reflection to make the generic type, you may as well not use a generic type at all and just use the following class:

public class DataField
{
    public string Name { get; set; }
    public object Value { get; set; }
}
Connell
  • 13,925
  • 11
  • 59
  • 92
1

You'll need to insert the logic for determining the data type from your XML and add all the types you need to use but this should work:

            result = (from d in XDocument.Parse(data.OuterXML).Root.Descendants()
                      let isString = true //Replace true with your logic to determine if it is a string.
                      let isInt = false   //Replace false with your logic to determine if it is an integer.
                      let stringValue = isString ? (DataField)new DataField<string>
                      {
                          Name = d.Name.ToString(),
                          Value = d.Value
                      } : null
                      let intValue = isInt ? (DataField)new DataField<int>
                      {
                          Name = d.Name.ToString(),
                          Value = Int32.Parse(d.Value)
                      } : null
                      select stringValue ?? intValue).ToList();
David
  • 320
  • 1
  • 5