How to deserialize dodgy JSON (with improperly quoted strings, and missing brackets)? - arrays

I am having to parse (and ultimately reserialize) some dodgy JSON. it looks like this:
{
name: "xyz",
id: "29573f59-85fb-4d06-9905-01a3acb2cdbd",
status: "astatus",
color: colors["Open"]
},
{
name: "abc",
id: "29573f59-85fb-4d06-9905-01a3acb2cdbd",
status: "astatus",
color: colors["Open"]
}
There are a number of problems here - starting with the most severe.
color: colors["Open"]
WTF even is that? If I drop 'colors' then I can get an array of strings out but I can't tweak to work out of the box.
It is an array without square brackets. I can fix this by wrapping in them. But is there a way to support out of the box?
Properties have no quotes. Deserializing is fine for these.. but reserializing is just no dice.
Any suggestions of handling both in and out of this structure?

Answering your questions #1 - #3 in order:
Json.NET does not support reading dodgy property values in the form colors["Open"] (which, as you correctly note, violates the JSON standard).
Instead, you will need to manually fix these values, e.g. through some sort of Regex:
var regex = new Regex(#"(colors\[)(.*)(\])");
var fixedJsonString = regex.Replace(jsonString,
m => string.Format(#"""{0}{1}{2}""", m.Groups[1].Value, m.Groups[2].Value.Replace("\"", "\\\""), m.Groups[3].Value));
This changes the color property values into properly escaped JSON strings:
color: "colors[\"Open\"]"
Json.NET does, however, have the capability to write dodgy property values by calling JsonWriter.WriteRawValue() from within a custom JsonConverter.
Define the following converter:
public class RawStringConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override bool CanRead { get { return false; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var s = (string)value;
writer.WriteRawValue(s);
}
}
Then define your RootObject as follows:
public class RootObject
{
public string name { get; set; }
public string id { get; set; }
public string status { get; set; }
[JsonConverter(typeof(RawStringConverter))]
public string color { get; set; }
}
Then, when re-serialized, you will get the original dodgy values in your JSON.
Support for deserializing comma-delimited JSON without outer brackets will be in the next release of Json.NET after 10.0.3. see Issue 1396 and Issue 1355 for details. You will need to set JsonTextReader.SupportMultipleContent = true to make it work.
In the meantime, as a workaround, you could grab ChainedTextReader and public static TextReader Extensions.Concat(this TextReader first, TextReader second) from the answer to How to string multiple TextReaders together? by Rex M and surround your JSON with brackets [ and ].
Thus you would deserialize your JSON as follows:
List<RootObject> list;
using (var reader = new StringReader("[").Concat(new StringReader(fixedJsonString)).Concat(new StringReader("]")))
using (var jsonReader = new JsonTextReader(reader))
{
list = JsonSerializer.CreateDefault().Deserialize<List<RootObject>>(jsonReader);
}
(Or you could just manually surround your JSON string with [ and ], but I prefer solutions that don't involve copying possibly large strings.)
Re-serializing a root collection without outer braces is possible if you serialize each item individually using its own JsonTextWriter with CloseOutput = false. You can also manually write a , between each serialized item to the underlying TextWriter shared by every JsonTextWriter.
Serializing JSON property names without a surrounding quote character is possible if you set JsonTextWriter.QuoteName = false.
Thus, to re-serialize your List<RootObject> without quoted property names or outer braces, do:
var sb = new StringBuilder();
bool first = true;
using (var textWriter = new StringWriter(sb))
{
foreach (var item in list)
{
if (!first)
{
textWriter.WriteLine(",");
}
first = false;
using (var jsonWriter = new JsonTextWriter(textWriter) { QuoteName = false, Formatting = Formatting.Indented, CloseOutput = false })
{
JsonSerializer.CreateDefault().Serialize(jsonWriter, item);
}
}
}
var reserializedJson = sb.ToString();
Sample .Net fiddle showing all this in action.

Related

MVC Custom Validation String Array Client Side

There are examples out there of custom MVC validators that take an array parameter, but only server side - none of them show an example of implementing the client side with array parameter.
The problem is instead of outputting the array's contents in the html data- attribute, it outputs "System.String[]":
data-val-total-propertynames="System.String[]"
Here is my attribute class:
public class TotalAttribute : ValidationAttribute, IClientValidatable
{
private String[] PropertyNames { get; set; }
public TotalAttribute(String[] propertyNames)
{
PropertyNames = propertyNames;
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
float total = 0;
foreach (var propertyName in PropertyNames)
total += (float)context.ObjectInstance.GetType().GetProperty(propertyName).GetValue(context.ObjectInstance, null);
if (total != (float)value)
return new ValidationResult(FormatErrorMessage(context.DisplayName), new[] { context.MemberName });
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = ErrorMessageString,
ValidationType = "total",
};
rule.ValidationParameters["propertynames"] = PropertyNames;
yield return rule;
}
}
Here it is implemented in the model:
[Total(new string[] { "SomeOtherField1", "SomeOtherField2" }, ErrorMessage = "'Line12Balance' must equal total of 'SomeOtherField1' and 'SomeOtherField2'")]
public decimal? Line12Balance { get; set; }
Here is the html data-val attribute output:
data-val-total-propertynames="System.String[]"
What am I doing wrong?
You get "System.String[]" string because the value of ValidationParameters["propertynames"] is written by calling ToString on it, so string[] variable returns "System.String[]" in this case. If you need to output specific value you need to format it by yourself in your validation attribute. For example, change
rule.ValidationParameters["propertynames"] = PropertyNames;
to
rule.ValidationParameters["propertynames"] = string.Join(",", PropertyNames);
and you will get
data-val-total-propertynames="SomeOtherField1,SomeOtherField2"
As Alexander mentioned in his answer, the issue you encountered is that string[] doesn't have its own ToString() implementation, and uses the base object.ToString() implementation, which just displays the type of the object rather than the contents of your array.
So you've got to somehow serialize your string array so that it can be stored in a string to then parse in your client-side JavaScript.
If you're already using JSON.Net in your solution (as many are), you can also do this by JSON serializing the array on the server side, then parsing the JSON client-side.
i.e. server-side:
rule.ValidationParameters["propertynames"] = JsonConvert.SerializeObject(PropertyNames);
Client-side: (
$.validator.addMethod('rule', function(value, element, params) {
var propertynames = JSON.parse(params.propertynames);
// do your validation
});
That prevents you from having to escape commas/delimiters in your parameters.

JAXB JSON array not in object

I would like to know how I can use JAXB to consume and produce a JSON array from a List without being included in an object:
[ "element1", "element2", "element3"]
Currently I have being only able to do so by including it in an object:
{ "elements" : [ "element1", "element2", "element3"] }
The code used is more or less the one below.
#XmlRootElement(name = "elements")
public class ElementListSerializerHelper
#XmlElement(name = "elements")
#JsonProperty(value = "elements")
public List<String> list;
public ElementsListSerializerHelper() {
list = new ArrayList<String>();
}
public ElementsListSerializerHelper(List<String> list) {
this.list = new ArrayList<String>(list);
}
}
Any help is more than welcome. The option I'm also thinking is generating/parsing a string based on jackson library.

mappingMongoConverter.setMapKeyDotReplacement doesnt seem to have any effects

I am trying to convert a custom object that contains a map containing dotted key string value using the latest 1.7.2 spring mongodb.
Setting a dot replacement doesnt seem to do the job. Here's my code :
class FakeUser {
Map<String, String> map = new LinkedHashMap<>();
void addValue(String key, String value) {
this.map.put(key, value);
}
}
FakeUser fakeUser = new FakeUser();
fakeUser.addValue("test.dot.for.key", "test.dot.for.value");
this.mappingMongoConverter.setMapKeyDotReplacement(":");
Object convertedObject = this.mappingMongoConverter.convertToMongoType(fakeUser);
System.out.println("convertedObject: " + convertedObject.getClass() + ":" + convertedObject);
And the output:
convertedObject: class com.mongodb.BasicDBObject:{ "map" : { "test.dot.for.key" : "test.dot.for.value"}}
And i also tried:
class FakeUser {
Map<String, String> map = new LinkedHashMap<>();
void addValue(String key, String value) {
this.map.put(key, value);
}
}
FakeUser fakeUser = new FakeUser();
fakeUser.addValue("test.dot.for.key", "test.dot.for.value");
this.mappingMongoConverter.setMapKeyDotReplacement(":");
BasicDBObject dbo = new BasicDBObject();
this.mappingMongoConverter.write(fakeUser, dbo);
System.out.println("dbo: " + ":" + dbo.toMap());
And with the output of dbo: :{_class=app.security.MyClass$1FakeUser, map={ "test.dot.for.key" : "test.dot.for.value"}}
I was expecting "test.dot.for.key" to become "test:dot:for:key", so what did i do wrong ?
You need make adjustments to the converter in your SpringMongoDBConfig (which should extend AbstractMongoConfiguration) config files, rather than in the class you're trying to serialize/deserialze. If you're using annotation driven settings, you can use a custom bean to set the converter like so:
#Bean
#Override
public MappingMongoConverter mappingMongoConverter() throws Exception
{
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext());
converter.setCustomConversions(customConversions());
//mongo won't accept key values with dots(.) in them, so configure it to store them as :
converter.setMapKeyDotReplacement("\\:");
return converter;
}
Once the converter is setup, it will handle both serialization and deserialization for you automagically.
If you really want to do the conversion in-line (not recommended), you can just use a string replace function to re-write the string like so:
class FakeUser
{
Map<String, String> map = new LinkedHashMap<>();
void addValue(String key, String value) {
key = key.replace(".",":");
this.map.put(key, value);
}
}
Although, if you do it inline you'll likely have issues deserializing when you get it back out of mongo.

Restrict Blocks in ContentArea

I'm having a issue restricting what kind of Block to be inserted in a ContentArea. What I want is that the SliderBlock's ContentArea property can only have insertion of a SlideItemBlock.
[ContentType(...)]
public class SlideItemBlock : BlockData
{
[Required]
Display(Name = "Image")]
public virtual string Image { get; set;}
}
[ContentType(...)]
public class SliderBlock : BlockData
{
[Required]
[Display(Name = "Slides")]
public virtual ContentArea Slides { get; set; }
//Should only accept insertion of SlideItemBlock
}
Or is this the wrong way to achive what I'm trying to restrict for the editor to not drag and drop wrong block types?
As of now, I can create a SliderBlock and insert a SlideItemBlocks in it. If I then insert the created SliderBlock in a new SliderBlock I get a forever and ever loop and It breaks the site. This is what I'm trying to control.
If you´re using EPiServer 7.5 restricting which blocks you can use in a content area is built in. For details take a look at this blog post: Restricting the allowed types in a content area.
Code example from the blog post:
[EditorDescriptorRegistration(TargetType = typeof(ContentArea), UIHint = "Gallery")]
public class ImageGalleryEditorDescriptor : EditorDescriptor
{
public ImageGalleryEditorDescriptor()
{
// Setup the types that are allowed to be dragged and dropped into the content
// area; in this case only images are allowed to be added.
AllowedTypes = new Type[] { typeof(IContentImage) };
// Unfortunetly the ContentAreaEditorDescriptor is located in the CMS module
// and thus can not be inherited from; these settings are copied from that
// descriptor. These settings determine which editor and overlay should be
// used by this property in edit mode.
ClientEditingClass = "epi-cms.contentediting.editors.ContentAreaEditor";
OverlayConfiguration.Add("customType", "epi-cms.widget.overlay.ContentArea");
}
}
As of EpiServer 8 theres a new attribute called [AllowedTypes]. This is now the best way of restricting blocks. It overcomes a lot of the limitations of [AvailableContentTypes]. When you drag blocks into a content area the validation actually works.
An example code snippet would be
[AllowedTypes(new []{ typeof(SlideBlock) })]
public virtual ContentArea Slides { get; set; }
Theres a good code example here How To Restrict The Blocks Allowed Within A Content Area Episerver
Also this one on EpiWorld http://world.episerver.com/blogs/Ben-McKernan/Dates/2015/2/the-new-and-improved-allowed-types/
Of you have not upgraded to 7.5 yet as Frederik suggest we have the following attribute we have created to do just this.
using EPiServer.Core;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace xxx.Com.Core.Attributes
{
[AttributeUsage(AttributeTargets.Property)]
public class OurAvailableContentTypesAttribute : ValidationAttribute
{
public Type[] Include { get; set; }
public Type[] Exclude { get; set; }
public override bool IsValid(object value)
{
if (value == null)
{
return true;
}
if (!(value is ContentArea))
{
throw new ValidationException("OurAvailableContentTypesAttribute is intended only for use with ContentArea properties");
}
var contentArea = value as ContentArea;
var notAllowedcontentNames = new List<string>();
if (contentArea != null)
{
if (Include != null)
{
var notAllowedContent = contentArea.Contents.Where(x => !ContainsType(Include, x.GetType()));
if (notAllowedContent.Any())
{
notAllowedcontentNames.AddRange(notAllowedContent.Select(x => string.Format("{0} ({1})", x.Name, x.ContentLink.ID)));
}
}
if (Exclude != null)
{
var notAllowedContent = contentArea.Contents.Where(x => ContainsType(Exclude, x.GetType()));
if (notAllowedContent.Any())
{
notAllowedcontentNames.AddRange(notAllowedContent.Select(x => string.Format("{0} ({1})", x.Name, x.ContentLink.ID)));
}
}
}
if (notAllowedcontentNames.Any())
{
ErrorMessage = "contains invalid content items :";
foreach (var notAllowedcontentName in notAllowedcontentNames)
{
ErrorMessage += " " + notAllowedcontentName + ",";
}
ErrorMessage = ErrorMessage.TrimEnd(',');
return false;
}
return true;
}
private bool ContainsType(Type[] include, Type type)
{
return include.Any(inc => inc.IsAssignableFrom(type));
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var result = base.IsValid(value, validationContext);
if (result != null && !string.IsNullOrEmpty(result.ErrorMessage))
{
result.ErrorMessage = string.Format("{0} {1}", validationContext.DisplayName, ErrorMessage);
}
return result;
}
}
}
the usage of this is then
public class OurBlock : BlockData
{
[CultureSpecific]
[Editable(true)]
[Display(Name = "",
Description = "",
GroupName = SiteConstants.GroupNames.ContentArea,
Order = 1)]
[OurAvailableContentTypes(Include = new[] { typeof(OurImageBlock) })]
public virtual ContentArea ImageContentArea { get; set; }
HTH
Adam
Create a validation class and implement the IValidate interface from EPiServer.validation. The validation of this is kept outside of the PageData and BlockData classes.
This should be what you are looking for
using System.Collections.Generic;
using System.Linq;
using EPiServer.Validation;
public class SliderBlockValidator : IValidate<SliderBlock>
{
public IEnumerable<ValidationError> Validate(SliderBlock instance)
{
var errors = new List<ValidationError>();
if (instance.Slides != null &&
instance.Slides.Contents.Any(x => x.GetType().BaseType != typeof (SlideItemBlock)))
{
errors.Add(new ValidationError()
{
ErrorMessage = "Only SlideItemBlocks are allowed in this area",
PropertyName = "Slides",
Severity = ValidationErrorSeverity.Error,
ValidationType = ValidationErrorType.StorageValidation
});
}
return errors;
}
}
More reading at http://sdkbeta.episerver.com/SDK-html-Container/?path=/SdkDocuments/CMS/7/Knowledge%20Base/Developer%20Guide/Validation/Validation.htm&vppRoot=/SdkDocuments//CMS/7/Knowledge%20Base/Developer%20Guide/
If you have upgraded to EPi 7.5 you can use the AllowedTypes annotation
[AllowedTypes(new [] {typeof(SlideItemBlock)})]
public virtual ContentArea Slides { get; set; }
I am unaware if you are able to customize any messages using the later solution. There are a few known limitations
Restriction does not work for overlays when editing on page. This is a bug that has been fixed and will be released in a patch in a few weeks.
No server validation. Currently, the attribute only adds restriction in the UI. We hope to be able to add support for server validation soon which would also give the posibility validate your custom properties.
No validation when creating local blocks in content areas. If you use the new feature to add local blocks to a content area, there is currently no filtering of the content types when you create your new block.
Read more at http://world.episerver.com/Blogs/Linus-Ekstrom/Dates/2013/12/Restriction-of-content-types-in-properties/
All in all the first solution is currently the better one.
You can add a validation attribute to the content area property to restrict the allowed block types.
See this link for a detailed example.
Then using the AvailableContentTypes attribute you can restrict to only allow SlideItemBlock types.
[Required]
[Display(Name = "Slides")]
[AvailableContentTypes(Include = new []{typeof(SlideItemBlock)})]
public virtual ContentArea Slides { get; set; }

How do I dynamically generate columns in a WPF DataGrid?

I am attempting to display the results of a query in a WPF datagrid. The ItemsSource type I am binding to is IEnumerable<dynamic>. As the fields returned are not determined until runtime I don't know the type of the data until the query is evaluated. Each "row" is returned as an ExpandoObject with dynamic properties representing the fields.
It was my hope that AutoGenerateColumns (like below) would be able to generate columns from an ExpandoObject like it does with a static type but it does not appear to.
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Results}"/>
Is there anyway to do this declaratively or do I have to hook in imperatively with some C#?
EDIT
Ok this will get me the correct columns:
// ExpandoObject implements IDictionary<string,object>
IEnumerable<IDictionary<string, object>> rows = dataGrid1.ItemsSource.OfType<IDictionary<string, object>>();
IEnumerable<string> columns = rows.SelectMany(d => d.Keys).Distinct(StringComparer.OrdinalIgnoreCase);
foreach (string s in columns)
dataGrid1.Columns.Add(new DataGridTextColumn { Header = s });
So now just need to figure out how to bind the columns to the IDictionary values.
Ultimately I needed to do two things:
Generate the columns manually from the list of properties returned by the query
Set up a DataBinding object
After that the built-in data binding kicked in and worked fine and didn't seem to have any issue getting the property values out of the ExpandoObject.
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Results}" />
and
// Since there is no guarantee that all the ExpandoObjects have the
// same set of properties, get the complete list of distinct property names
// - this represents the list of columns
var rows = dataGrid1.ItemsSource.OfType<IDictionary<string, object>>();
var columns = rows.SelectMany(d => d.Keys).Distinct(StringComparer.OrdinalIgnoreCase);
foreach (string text in columns)
{
// now set up a column and binding for each property
var column = new DataGridTextColumn
{
Header = text,
Binding = new Binding(text)
};
dataGrid1.Columns.Add(column);
}
The problem here is that the clr will create columns for the ExpandoObject itself - but there is no guarantee that a group of ExpandoObjects share the same properties between each other, no rule for the engine to know which columns need to be created.
Perhaps something like Linq anonymous types would work better for you. I don't know what kind of a datagrid you are using, but binding should should be identical for all of them. Here is a simple example for the telerik datagrid.
link to telerik forums
This isn't actually truly dynamic, the types need to be known at compile time - but this is an easy way of setting something like this at runtime.
If you truly have no idea what kind of fields you will be displaying the problem gets a little more hairy. Possible solutions are:
Creating a type mapping at runtime by using Reflection.Emit, I think it's possible to create a generic value converter that would accept your query results, create a new type (and maintain a cached list), and return a list of objects. Creating a new dynamic type would follow the same algorithm as you already use for creating the ExpandoObjectsMSDN on Reflection.Emit
An old but useful article on codeproject
Using Dynamic Linq - this is probably the simpler faster way to do it.Using Dynamic Linq
Getting around anonymous type headaches with dynamic linq
With dynamic linq you can create anonymous types using a string at runtime - which you can assemble from the results of your query. Example usage from the second link:
var orders = db.Orders.Where("OrderDate > #0", DateTime.Now.AddDays(-30)).Select("new(OrderID, OrderDate)");
In any case, the basic idea is to somehow set the itemgrid to a collection of objects whose shared public properties can be found by reflection.
my answer from Dynamic column binding in Xaml
I've used an approach that follows the pattern of this pseudocode
columns = New DynamicTypeColumnList()
columns.Add(New DynamicTypeColumn("Name", GetType(String)))
dynamicType = DynamicTypeHelper.GetDynamicType(columns)
DynamicTypeHelper.GetDynamicType() generates a type with simple properties. See this post for the details on how to generate such a type
Then to actually use the type, do something like this
Dim rows as List(Of DynamicItem)
Dim row As DynamicItem = CType(Activator.CreateInstance(dynamicType), DynamicItem)
row("Name") = "Foo"
rows.Add(row)
dataGrid.DataContext = rows
Although there is an accepted answer by the OP, it uses AutoGenerateColumns="False" which is not exactly what the original question asked for. Fortunately, it can be solved with auto-generated columns as well. The key to the solution is the DynamicObject that can have both static and dynamic properties:
public class MyObject : DynamicObject, ICustomTypeDescriptor {
// The object can have "normal", usual properties if you need them:
public string Property1 { get; set; }
public int Property2 { get; set; }
public MyObject() {
}
public override IEnumerable<string> GetDynamicMemberNames() {
// in addition to the "normal" properties above,
// the object can have some dynamically generated properties
// whose list we return here:
return list_of_dynamic_property_names;
}
public override bool TryGetMember(GetMemberBinder binder, out object result) {
// for each dynamic property, we need to look up the actual value when asked:
if (<binder.Name is a correct name for your dynamic property>) {
result = <whatever data binder.Name means>
return true;
}
else {
result = null;
return false;
}
}
public override bool TrySetMember(SetMemberBinder binder, object value) {
// for each dynamic property, we need to store the actual value when asked:
if (<binder.Name is a correct name for your dynamic property>) {
<whatever storage binder.Name means> = value;
return true;
}
else
return false;
}
public PropertyDescriptorCollection GetProperties() {
// This is where we assemble *all* properties:
var collection = new List<PropertyDescriptor>();
// here, we list all "standard" properties first:
foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(this, true))
collection.Add(property);
// and dynamic ones second:
foreach (string name in GetDynamicMemberNames())
collection.Add(new CustomPropertyDescriptor(name, typeof(property_type), typeof(MyObject)));
return new PropertyDescriptorCollection(collection.ToArray());
}
public PropertyDescriptorCollection GetProperties(Attribute[] attributes) => TypeDescriptor.GetProperties(this, attributes, true);
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string GetClassName() => TypeDescriptor.GetClassName(this, true);
public string GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[] attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object GetPropertyOwner(PropertyDescriptor pd) => this;
}
For the ICustomTypeDescriptor implementation, you can mostly use the static functions of TypeDescriptor in a trivial manner. GetProperties() is the one that requires real implementation: reading the existing properties and adding your dynamic ones.
As PropertyDescriptor is abstract, you have to inherit it:
public class CustomPropertyDescriptor : PropertyDescriptor {
private Type componentType;
public CustomPropertyDescriptor(string propertyName, Type componentType)
: base(propertyName, new Attribute[] { }) {
this.componentType = componentType;
}
public CustomPropertyDescriptor(string propertyName, Type componentType, Attribute[] attrs)
: base(propertyName, attrs) {
this.componentType = componentType;
}
public override bool IsReadOnly => false;
public override Type ComponentType => componentType;
public override Type PropertyType => typeof(property_type);
public override bool CanResetValue(object component) => true;
public override void ResetValue(object component) => SetValue(component, null);
public override bool ShouldSerializeValue(object component) => true;
public override object GetValue(object component) {
return ...;
}
public override void SetValue(object component, object value) {
...
}

Resources