3

The following code does not seem to work, although according to documentation the part with boxing should work. I seem not be able to find more information on this than the article at Value Type Property setting

This question is also not similar as in Setting a property of a property in a struct? as this has nothing to do with reflection, which I am trying to use.

The code is just a small piece of a much larger project (using reflection to get something done) but demonstrates the issue.

The specialty in this example is that the property we want to set is in a value type (struct) and SetValue seems not to be handling this very well...

Imports System.Drawing

Module Module1
    Sub Main()
        Console.WriteLine("Starting test")
        Dim s As Size
        Console.WriteLine("Created size: {0}", s)
        s.Width = 5
        Console.WriteLine("Set Width: {0}", s)
        Dim pi = s.GetType().GetProperty("Height")
        Console.WriteLine("get property info: {0}", pi)
        pi.SetValue(s, 10, Nothing)
        Console.WriteLine("setting height with pi: {0}", s)
        Dim box As Object = s 'same result with  RuntimeHelpers.GetObjectValue(s)
        Console.WriteLine("boxing: {0}", box)
        pi.SetValue(box, 10, Nothing)
        Console.WriteLine("setting value: {0}", box)
        Console.WriteLine("")
        Console.WriteLine("End test")
        Console.ReadLine()
    End Sub
End Module

The result given is:

Starting test
Created size: {Width=0, Height=0}
Set Width: {Width=5, Height=0}
get property info: Int32 Height
setting height with pi: {Width=5, Height=0} 'expected 5,10
boxing: {Width=5, Height=0}
setting value: {Width=5, Height=0} 'expected 5,10

End test

The same code is working (with boxing) in C#

    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Starting test");
            Size s= new Size();
            Console.WriteLine("Created size: {0}", s);
            s.Width = 5;
            Console.WriteLine("Set Width: {0}", s);
            PropertyInfo pi = s.GetType().GetProperty("Height");
            Console.WriteLine("get property info: {0}", pi);
            Console.WriteLine("can write?: {0}", pi.CanWrite);
            pi.SetValue(s, 10, null);
            Console.WriteLine("setting height with pi: {0}", s);
            Object box = s; //same result with  RuntimeHelpers.GetObjectValue(s)
            Console.WriteLine("boxing: {0}", box);
            pi.SetValue(box, 10, null);
            Console.WriteLine("setting value: {0}", box);
            Console.WriteLine("");
            Console.WriteLine("End test");
            Console.ReadLine();
        }
    }
Starting test
Created size: {Width=0, Height=0}
Set Width: {Width=5, Height=0}
get property info: Int32 Height
can write?: True
setting height with pi: {Width=5, Height=0}
boxing: {Width=5, Height=0}
setting value: {Width=5, Height=10}

End test

How do I make this work in VB?

Johan
  • 95
  • 7
  • `Size` is a `struct`, not a `class`, so it has _value-type semantics_, which means you can't simply set a single property on a `struct` local (`s`) and expect it to work like a property in a `class`. – Dai Aug 09 '23 at 07:38
  • @Dai I know, but I am trying to find a way to make this work, that's partially where the boxing comes from, Boxing should create an object with a shallow copy and that should work according to the article mentioned. – Johan Aug 09 '23 at 07:52
  • 1
    I think you have yet to understand how value-types work then, because if you did understand that then you wouldn't be "trying to make this work" because there is **no reason** for doing things this way - which leads me on to ask you what actual problem you're _really_ trying to solve here? – Dai Aug 09 '23 at 07:53
  • The c# code is part of a small library part of a much bigger VB project (don't ask). As it is needed for future improvements to alter a part of this code, it was decided to convert the c# code to VB and put it in another larger library where it makes much more sense. As this problem is now the only showstopper (what is the difference between c# and VB in this case???) I would really like to have a solution, so please do not close this question... – Johan Aug 09 '23 at 08:38
  • _"it was decided to convert the c# code to VB"_ <-- Sorry, but this is simply a _bad idea_: VB.NET is a dead-language-walking at this point. – Dai Aug 09 '23 at 08:39
  • 2
    may be so, but unless somebody want to change all VB code of this project (8 solutions with each 5-20 projects with tons of code) for free, this will probably remain... – Johan Aug 09 '23 at 08:45
  • You've convinced me to reopen it for you now, but I can't really provide any further guidance because I still don't understand what **actual** problem you're trying to solve... – Dai Aug 09 '23 at 08:47
  • 1
    Thanks, I am just looking for 1-Why is the behavior of c# and VB different in this case 2- How do i get the vb version working – Johan Aug 09 '23 at 08:50
  • I did some quick digging and from what I can tell, the VB.NET compiler is inserting hidden calls to `RuntimeHelpers.GetObjectValue` _right before_ every call to `PropertyInfo.SetValue` , which causes the boxed struct to be "re-boxed" and so you lose your changes. – Dai Aug 09 '23 at 09:11

1 Answers1

3

TL;DR: In VB.NET you need to use System.ValueType when working with boxed mutable structs instead of System.Object (aka Object in VB.NET and object in C#).

Change your code to this and it works (for me, at least, in the VB.NET in Linqpad 7 targeting .NET 7):

(You'll want to replace the .Dump() calls with Console.WriteLine or similar)

Dim heightProperty = GetType( Size ).GetProperty( "Height", BindingFlags.Public Or BindingFlags.Instance )

Dim localStruct As Size
localStruct.Width = 5
localStruct.Dump( "Should have Width 5" )

' This `SetValue` call below won't work because `vbc` will insert a hidden `PropertyInfo.SetValue(Object, Object)` call which will box `localStruct` such that you can't retrieve it after it's mutated, so effectively discarding it.
heightProperty.SetValue( localStruct, 10 )
localStruct.Dump( ( "Should have Height 10, but has " + localStruct.ToString() ) )

Dim boxedAsObject As System.Object = localStruct
LINQPad.Extensions.Dump( boxedAsObject, "boxed" ) ' interestingly, you can't use extension-method syntax on Object-typed boxed structs, weird.

heightProperty.SetValue( boxedAsObject, 10) ' This won't work because `vbc` will insert a hidden `PropertyInfo.SetValue(Object, Object)` call which will reset `boxedAsObject`.
LINQPad.Extensions.Dump( boxedAsObject, ( "Should have Height 10, but has " + boxedAsObject.ToString() )  )

' https://www.pcreview.co.uk/threads/cannot-get-propertyinfo-setvalue-to-work-for-a-structure.1418755/
Dim boxedAsValueType As System.ValueType = localStruct
heightProperty.SetValue( boxedAsValueType, 10 )
boxedAsValueType.Dump( "Will have Height = 10: " + boxedAsValueType.ToString() )

Gives me this output:

enter image description here


VB.NET has its own special rules for handling boxed structs compared to C#: when you use System.Object as a _quasi-_top-type for boxed structs in VB.NET then the IL compiler will insert hidden calls to RuntimeHelpers.GetObjectValue which will cause the boxed struct reference on the stack to be replaced with a new boxed struct instance.

(The IL below was compiled with optimizations enabled)

IL_0000 ldtoken Size
IL_0005 call    Type.GetTypeFromHandle (RuntimeTypeHandle)
IL_000A ldstr   "Height"
IL_000F ldc.i4.s    14  // 20
IL_0011 call    Type.GetProperty (String, BindingFlags)
IL_0016 ldloca.s    00    // localStruct
IL_0018 ldc.i4.5    
IL_0019 call    Size.set_Width (Int32)
IL_001E ldloc.0    // localStruct
IL_001F ldstr   "Should have Width 5"
IL_0024 call    Extensions.Dump<Size> (Size, String)
IL_0029 pop 
IL_002A dup 
IL_002B ldloc.0    // localStruct
IL_002C box Size
IL_0031 ldc.i4.s    0A  // 10
IL_0033 box Int32
IL_0038 callvirt    PropertyInfo.SetValue (Object, Object)
IL_003D ldloc.0    // localStruct
IL_003E ldstr   "Should have Height 10, but has "
IL_0043 ldloca.s    00    // localStruct
IL_0045 constrained.    Size
IL_004B callvirt    Object.ToString ()
IL_0050 call    String.Concat (String, String)
IL_0055 call    Extensions.Dump<Size> (Size, String)
IL_005A pop 
IL_005B ldloc.0    // localStruct
IL_005C box Size
IL_0061 stloc.1    // boxedAsObject
IL_0062 ldloc.1    // boxedAsObject
IL_0063 call    RuntimeHelpers.GetObjectValue (Object)
IL_0068 ldstr   "boxed"
IL_006D call    Extensions.Dump<Object> (Object, String)
IL_0072 pop 
IL_0073 dup 
IL_0074 ldloc.1    // boxedAsObject
IL_0075 call    RuntimeHelpers.GetObjectValue (Object)
IL_007A ldc.i4.s    0A  // 10
IL_007C box Int32
IL_0081 callvirt    PropertyInfo.SetValue (Object, Object)
IL_0086 ldloc.1    // boxedAsObject
IL_0087 call    RuntimeHelpers.GetObjectValue (Object)
IL_008C ldstr   "Should have Height 10, but has "
IL_0091 ldloc.1    // boxedAsObject
IL_0092 callvirt    Object.ToString ()
IL_0097 call    String.Concat (String, String)
IL_009C call    Extensions.Dump<Object> (Object, String)
IL_00A1 pop 
IL_00A2 ldloc.0    // localStruct
IL_00A3 box Size
IL_00A8 stloc.2    // boxedAsValueType
IL_00A9 ldloc.2    // boxedAsValueType
IL_00AA ldc.i4.s    0A  // 10
IL_00AC box Int32
IL_00B1 callvirt    PropertyInfo.SetValue (Object, Object)
IL_00B6 ldloc.2    // boxedAsValueType
IL_00B7 ldstr   "Will have Height = 10: "
IL_00BC ldloc.2    // boxedAsValueType
IL_00BD callvirt    ValueType.ToString ()
IL_00C2 call    String.Concat (String, String)
IL_00C7 call    Extensions.Dump<ValueType> (ValueType, String)
IL_00CC pop 
IL_00CD ret 

...whereas the IL for the equivalent C# lacks those hidden calls:

IL_0000 ldtoken Size
IL_0005 call    Type.GetTypeFromHandle (RuntimeTypeHandle)
IL_000A ldstr   "Height"
IL_000F ldc.i4.s    14  // 20
IL_0011 call    Type.GetProperty (String, BindingFlags)
IL_0016 ldloca.s    00    // localStruct
IL_0018 initobj Size
IL_001E ldloca.s    00    // localStruct
IL_0020 ldc.i4.5    
IL_0021 call    Size.set_Width (Int32)
IL_0026 ldloc.0    // localStruct
IL_0027 ldstr   "Should have Width 5"
IL_002C call    Extensions.Dump<Size> (Size, String)
IL_0031 pop 
IL_0032 dup 
IL_0033 ldloc.0    // localStruct
IL_0034 box Size
IL_0039 ldc.i4.s    0A  // 10
IL_003B box Int32
IL_0040 callvirt    PropertyInfo.SetValue (Object, Object)
IL_0045 ldloc.0    // localStruct
IL_0046 ldstr   "Should have Height 10"
IL_004B call    Extensions.Dump<Size> (Size, String)
IL_0050 pop 
IL_0051 ldloc.0    // localStruct
IL_0052 box Size
IL_0057 stloc.1    // boxedAsObject
IL_0058 dup 
IL_0059 ldloc.1    // boxedAsObject
IL_005A ldc.i4.s    0A  // 10
IL_005C box Int32
IL_0061 callvirt    PropertyInfo.SetValue (Object, Object)
IL_0066 ldloc.1    // boxedAsObject
IL_0067 ldstr   "boxedAsObject"
IL_006C call    Extensions.Dump<Object> (Object, String)
IL_0071 pop 
IL_0072 ldloc.0    // localStruct
IL_0073 box Size
IL_0078 stloc.2    // boxedAsValueType
IL_0079 ldloc.2    // boxedAsValueType
IL_007A ldc.i4.s    0A  // 10
IL_007C box Int32
IL_0081 callvirt    PropertyInfo.SetValue (Object, Object)
IL_0086 ldloc.2    // boxedAsValueType
IL_0087 ldstr   "boxedAsValueType"
IL_008C call    Extensions.Dump<ValueType> (ValueType, String)
IL_0091 pop 
IL_0092 ret
Dai
  • 141,631
  • 28
  • 261
  • 374
  • Thanks @Dai. After modifying the question and adding the C# example i also went to look at the IL and came to the same conclusion after reading a lot about boxing (the question [link](https://stackoverflow.com/questions/26612912/boxing-behaves-differently-in-c-sharp-and-vb) gave me the answer i needed to solve the problem, but it was a little too late yesterday to post the results. But i wonder a little bit: should the C# code not be more 'correct' by also using the ValueType to do the boxing instead of the object, the object looks to me as a bit of corner cutting? – Johan Aug 10 '23 at 06:32
  • 1
    @Johan It embarrasses me to admit that despite writing in C# as my bread-and-butter for the past 18 years that I was still unfamiliar with `System.ValueType` until I wrote this answer yesterday - and yes, I think you're right: the C# code probably should also use `ValueType` (if only to act as an indication of intent) - that said, I think C#/.NET's boxing concept is _broken_ because in order to box a value you're forced to lose static type information which shouldn't be necessary at all: instead something like the `ref` syntax could have been used to denote a boxed value without `Object`. – Dai Aug 10 '23 at 06:39