XAML serialization - specifying a property is required - c#

I'm trying to use XAML to serialize/deserialize some custom (non-WPF/UI) information, and would like to enforce that certain properties are required.
XAML deserialization, by default, just creates each object with the default constructor, and then sets any properties it finds in the element's attributes, or in property element syntax. Any properties of the underlying object which are not specified in the XAML being serialized are just left as they are, i.e. whatever value they got after construction.
I'd like to know the best way of specifying that a certain property must be present in the XAML - and if not, have the deserialization fail.
I was expecting an attribute of some kind, but I can't find anything.
There are certain types in WPF which do exhibit this behaviour, but presumably WPF uses its own custom way of enforcing this. For example if you have..
<Setter Property="Height" ></Setter>
..the designer will complain 'The property "Value" is missing'.
I can think of some pretty convoluted ways of doing this:
Have each property setter somehow record it was called, and then have custom code run after deserialization which checks that all 'required' properties were actually set.
Use nullable properties everywhere, and then check after deserialization if any 'required' ones are still null. (Of course this won't work if null is a valid thing to set something to!)
Perhaps there's a way of writing a custom XamlObjectWriter which can check for some [Required] attribute on the object's properties, and fail if these are not found by the XamlReader.
These all sound like a lot more work than I hoped - and I'm sure there's a more obvious way. Does anyone have any other ideas, or experience of (and maybe a solution to) this problem?

I know this is old, but I ran across the question while looking for a way to do this shortly before I figured it out. Maybe it'll help someone else. Fortunately this is easy: implement ISupportInitialize. I'm using WPF but it should work anywhere.
public class Hero : ISupportInitialize
{
public string Who
{ get; set; } = string.Empty;
public string What
{ get; set; } = string.Empty;
public string Where
{ get; set; } = string.Empty;
public void BeginInit()
{
// set a flag here if your property setters
// have dependencies on other properties
}
public void EndInit()
{
if (string.IsNullOrWhiteSpace(Who))
throw new Exception($"The property \"Who\" is missing");
if (string.IsNullOrWhiteSpace(What))
throw new Exception($"The property \"What\" is missing");
// Where is optional...
}
}
Who and What are required, but What is missing on the second entry:
<Window.Resources>
<local:Hero x:Key="Badguy" Who="Vader" What="Sith" Where="Death Star"/>
<local:Hero x:Key="Goodguy" Who="HanSolo" Where="Millenium Falcon"/>
</Window.Resources>
In the VS2017 XAML markup editor:

I was facing a similar problem recently. After not being able to find any straightforward way, I decided to hook into the events of XamlObjectWriter to add custom support for this. It was basically what you suggested in point 3, except it turned out not really that complicated.
Basically it works like this: a dictionary is kept where each deserialized object is mapped to a set of remaining required properties. The BeforePropertiesHandler fills this set for the current object with all its properties with the RequiredAttribute. The XamlSetValueHandler removes the current property from the set. Finally, the AfterPropertiesHandler makes sure that there are no required properties left not set on the current object and throws an exception otherwise.
class RequiredAttribute : Attribute
{
}
public T Deserialize<T>(Stream stream)
{
var requiredProperties = new Dictionary<object, HashSet<MemberInfo>>();
var writerSettings = new XamlObjectWriterSettings
{
BeforePropertiesHandler = (sender, args) =>
{
var thisInstanceRequiredProperties = new HashSet<MemberInfo>();
foreach(var propertyInfo in args.Instance.GetType().GetProperties())
{
if(propertyInfo.GetCustomAttribute<RequiredAttribute>() != null)
{
thisInstanceRequiredProperties.Add(propertyInfo);
}
}
requiredProperties[args.Instance] = thisInstanceRequiredProperties;
},
XamlSetValueHandler = (sender, args) =>
{
if(!requiredProperties.ContainsKey(sender))
{
return;
}
requiredProperties[sender].Remove(args.Member.UnderlyingMember);
},
AfterPropertiesHandler = (sender, args) =>
{
if(!requiredProperties.ContainsKey(args.Instance))
{
return;
}
var propertiesNotSet = requiredProperties[args.Instance];
if(propertiesNotSet.Any())
{
throw new Exception("Required property \"" + propertiesNotSet.First().Name + "\" not set.");
}
requiredProperties.Remove(args.Instance);
}
};
var readerSettings = new XamlXmlReaderSettings
{
LocalAssembly = Assembly.GetExecutingAssembly(),
ProvideLineInfo = true
};
using(var reader = new XamlXmlReader(stream, readerSettings))
using(var writer = new XamlObjectWriter(reader.SchemaContext, writerSettings))
{
XamlServices.Transform(reader, writer);
return (T)writer.Result;
}
}

Related

Property with JsonIgnore attribute not ignored when deserializing with MissingMemberHandling.Error

I'm currently using Newtonsoft.Json 11.0.2 but I've also validated that this behavior is consistent starting 8.0.1~current. To my understanding, JsonIgnore Attribute is used as a hint to NOT serialize/deserialize. However, when combined with MissingMemberHandling.Error in JsonSerializationSettings, it is showing odd behavior when deserializing. I want to know if this a bug in Newtonsoft.Json or if there is an explanation for this strange behavior.
Here's a sample class:
public class Sample
{
public string Id {get; set;}
[JsonIgnore]
public string SubscriptionId { get; set;}
}
And a sample string to deserialize:
{
"id":"testId",
"subscriptionId":"testSubscriptionId"
}
[CASE1] Using the default constructor, deserializing does not throw any errors. It only sets the id and keeps subscriptionId null.
[CASE2] When I add a JsonConstructor only with the id, it will throw. Could not find member 'subscriptionId' on object of type 'Sample'.
[JsonConstructor]
public Sample(string id)
{
this.Id = id;
}
[CASE3] When I add a JsonConstructor with id and subscriptionId, it does not throw.
[JsonConstructor]
public Sample(string id, string subscriptionId)
{
this.Id = id;
}
I've looked through Newtonsoft.Json code here ln2196 (https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs),
CreatorPropertyContext creatorPropertyContext = new CreatorPropertyContext(memberName)
{
ConstructorProperty = contract.CreatorParameters.GetClosestMatchProperty(memberName),
Property = contract.Properties.GetClosestMatchProperty(memberName)
};
propertyValues.Add(creatorPropertyContext);
JsonProperty? property = creatorPropertyContext.ConstructorProperty ?? creatorPropertyContext.Property;
if (property != null && !property.Ignored)
{
// logic for populating the value
}
else
{
// if missing member handling is true, throw
}
[CASE3] makes sense to me because the constructor is explicitly anticipating the subscriptionId, so it can override the JsonIgnore (and is considered not ignored). [CASE2] makes sense to me too, because JsonIgnore attribute is set, it should not be considered when deserializing. However, why does [CASE1] not consider this property ignored?
==========
Edit:
Looking more through how json properties get initialized in the default constructor case, it seems that it is intentional that json ignore fields should not be considered as a missing field. When using the default constructor, JsonSerializerInternalReader.PopulateObject goes through the second path of Ignored, not the first path where it doesn't find the property.
JsonProperty? property = contract.Properties.GetClosestMatchProperty(propertyName);
if (property == null)
{
// if property is not found and MissingMemberHandling.Error is set, throw
}
if (property.Ignored || !ShouldDeserialize(reader, property, newObject))
{
// if marked ignored, don't throw, and also don't set.
}
So now I believe I've hit an obscure bug in Newtonsoft.Json and that [CASE2] should actually align with [CASE1] and not throw.
Here's a pull request to fix it:
https://github.com/JamesNK/Newtonsoft.Json/pull/2224

XML Serialization of Unknown Attributes in .NET Core

Over the past day or so I have been upgrading my application to .NET Core, netcoreapp1.0, from net451. Most of the process went very well and everything is ported over minus one issue I am seeing.
I was using System.Xml and System.Xml.Serialization to serialize XML from an API endpoint (which I have absolutely no control over). It was working well, but, due to an issue with the XML, there was always one attribute that came back as an UnknownAttribute. I handled it by wiring to the UknownAttribute event like this:
XmlReader reader = XmlReader.Create(stream);
var serializer = new XmlSerializer(typeof(PersonDetails));
var personDetails = (PersonDetails)serializer.Deserialize(reader);
serializer.UnknownAttribute += new XmlAttributeEventHandler((object sender, XmlAttributeEventArgs e) => {
unknownAttr.Add(e.Attr.Name, e.Attr.Value);
});
// Handle the missing map value if it exists.
string addressValue;
if(unknownAttr.TryGetValue("Address_x0_", out addressValue))
{
personDetails.Address = addressValue;
}
With the upgrade to .NET Core, I now use the System.Xml.XmlSerializer 4.0.11 library. Now, it appears that the UnknownAttribute event is no longer part of the XmlSerializer anymore. I have been unable to find another way to handle unknown attributes and would appreciate a pointer in the right direction.
Rather than using an unknown attribute event, you can add a public XmlAttribute[] XmlAttributes property to your PersonDetails, mark it with [XmlAnyAttribute], and handle the unknown attribute there:
public class PersonDetails
{
[XmlAttribute]
public string Address { get; set; }
[XmlAnyAttribute]
public XmlAttribute[] XmlAttributes
{
get
{
return null;
}
set
{
if (value != null)
{
foreach (var attr in value)
if (attr.Name == "Address_x0_")
Address = attr.Value;
}
}
}
}
As you found, you will need to add https://www.nuget.org/packages/System.Xml.XmlDocument/ to make use of XmlAttribute in .NET Core.

MvcSiteMapProvider Visibility directives in annotation doesn't work when implementing custom visibility provider

I'm configuring MvcSiteMapProvider with C# annotations and not through XML. I implemented a custom visibility provider based on the documentation. I derive my class from FilteredSiteMapNodeVisibilityProvider:
public class CustomVisibilityProvider: FilteredSiteMapNodeVisibilityProvider
{
public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
{
if (node.Attributes.Keys.Contains("customVisibility"))
{
string customVisibility = (string)node.Attributes["customVisibility"];
if (!string.IsNullOrEmpty(customVisibility))
{
customVisibility = customVisibility.Trim();
...
var criteria = ...
return criteria && base.IsVisible(node, sourceMetadata);
}
}
return base.IsVisible(node, sourceMetadata);
}
}
My Controller's view:
[MvcSiteMapNode(Title = "My View", ParentKey = "ParentController", Key = "MyView", Order = 922, PreservedRouteParameters = "id", Attributes = #"{ ""Visibility"": ""SiteMapPathHelper,!*"" }")]
public ActionResult MyView(int? id)
{
return ViewForEntity(id);
}
As we can see I didn't use my own customVisibility attribute with this view, but I'd like to use the standard Visibility attributes. This particular view shouldn't appear in the menu or elsewhere, except in the SiteMap.
The problem is that when this view's SiteMapNode is examined for visibility in the menu (a.k.a. (string)sourceMetadata["HtmlHelper"] == "MvcSiteMapProvider.Web.Html.SiteMapPathHelper"), the base.IsVisible(node, sourceMetadata) returns true. I'd expect that the FilteredSiteMapNodeVisibilityProvider will handle the Visibility attribute and return false, seeing that this view should only appear in the SiteMap.
As a workaround I currently implemented my own check:
private bool checkDefaultVisibility(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
{
bool defaultVisibility = sourceMetadata["HtmlHelper"] == null || !node.Attributes.Keys.Contains("Visibility");
if (sourceMetadata["HtmlHelper"] != null && node.Attributes.Keys.Contains("Visibility"))
{
var htmlHelper = (string)sourceMetadata["HtmlHelper"]; // Example: "MvcSiteMapProvider.Web.Html.SiteMapPathHelper"
var helpersRules = ((string)node.Attributes["Visibility"]).Split(',');
foreach(var helperRule in helpersRules)
{
if (helperRule != "!*" && htmlHelper.EndsWith("." + helperRule))
{
defaultVisibility = true;
break;
}
}
}
return defaultVisibility;
}
This is a method of my custom visibility provider. I hate it, because it's not universal, only handles specific cases. At the same time I don't want to reinvent the wheel here. I want Visibility to be handled by the MvcSiteMapProvider internals. How to achieve that?
Dictionary keys are case sensitive. The reason why FilteredSiteMapVisibilityProvider is not returning false is because you have set an attribute named Visibility instead of the expected name visibility.
[MvcSiteMapNode(Title = "My View", ParentKey = "ParentController", Key = "MyView", Order = 922, PreservedRouteParameters = "id", Attributes = #"{ ""visibility"": ""SiteMapPathHelper,!*"" }")]
As far as making visibility "universal", that would be difficult to do since everyone has different visibility requirements. The purpose of visibility providers is to implement your own visibility requirements. FilteredSiteMapNodeVisibilityProvider is about as generic as it can get, and if that doesn't meet your requirements you need to go custom.
Do note you can override the default visibility provider for specific nodes by setting the VisibilityProvider to the type string of the custom visibility provider class.
[MvcSiteMapNode(Title = "My View", ParentKey = "ParentController", Key = "MyView", Order = 922, PreservedRouteParameters = "id", VisibilityProvider = "MyNamespace.CustomVisibilityProvider, MyAssembly", Attributes = #"{ ""customVisibility"": ""foo"" }")]
If you need to use more than one visibility provider on the same node, it can be done using external DI and the CompositeSiteMapNodeVisibilityProvider as shown here.
Note that you can inherit this class for use with the internal DI container if you need to - but for internal DI you need a default constructor, so the types that it uses internally must be hard-coded into the constructor. But you can create as many of these classes as you need for your entire visibility configuration.
using MvcSiteMapProvider;
using MvcSiteMapProvider.Reflection;
public class MyCompositeVisibilityProvider : CompositeSiteMapNodeVisibilityProvider
{
public MyCompositeVisibilityProvider()
: base(
typeof(MyCompositeVisibilityProvider).ShortAssemblyQualifiedName(),
// Note that the visibility providers are executed in
// the order specified here, but execution stops when
// the first visibility provider returns false.
new FilteredSiteMapNodeVisibilityProvider(),
new TrimEmptyGroupingNodesVisibilityProvider(),
new CustomVisibilityProvider()
)
{ }
}
And then calling it by using:
[MvcSiteMapNode(Title = "My View", ParentKey = "ParentController", Key = "MyView", Order = 922, PreservedRouteParameters = "id", VisibilityProvider = "MyNamespace.MyCompositeVisibilityProvider, MyAssembly", Attributes = #"{ ""visibility"": ""SiteMapPathHelper,!*"", ""customVisibility"": ""foo"" }")]
Also note there are many other ways to control visibility including security trimming, customizing the templates (or creating new templates and specifying the templateName in the HTML helper explicitly) in the /Views/Shared/DisplayTemplates/ folder, or even creating custom HTML helpers.

Readonly tag MVC depending of a condition

My model has an property whcih I assigned a ReadOnly tag. My intention is to set a propery to readonly true or false depending of a condition like
class Test {
static bool test() { // It is my model
// My logical response ...
return true;
}
[ReadOnly(test)]
datetime prop {get; set;}
}
using this model I get the error message:
Error 7 An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter
Could you, pleaee, get me an idea for this?
=====================================================================================
Solution like answer 3:
Inside Template:
cshtml:
...
#if (Model.condition) {
<td>#Html.EditorFor(m => m.prop)</td>
} else {
<td>#Html.DisplayFor(m => m.prop)</td>
}
...
It will be inside the template.
Inside Model in the copmnstructor I set the condition of the property condition:
class XX {
public condition { get; set; } // not necessary readonly, I do the logical inside the template.
public datetime prop {get; set; }
public XX (bool _condition) {
condition = _condition;
}
}
You cannot use something other than described in the error message as the parameter for attributes.
It is a sad true, but still a true.
Only this:
[ReadOnly(5)]
[ReadOnly("string")] // Or other type (int/long/byte/etc..) which can be used with const keyword.
[ReadOnly(Enums.SomeValue)]
[ReadOnly(typeof(SomeType))]
[ReadOnly(new string[] { "array", "of", "strings"} )]
So unfortunately, you wont succeed making custom parameter type:
class ReadOnlyAttribute { ReadOnlyAttribute(MyClass foo) { ... } }
One alternative would be to do it within the get/set, something like:
class test
{
static bool test() {
...
}
private datetime prop;
public datetime Prop
{
get { return prop; }
set
{
if (test)
prop = value;
else
// Ignore, throw exception, etc.
}
}
}
The metadata for the model (which includes your IsReadOnly) is created by the Model Metadata providers. This providers only have information about data types, and property names, but not about the concrete values of the properties of an instance of the model. So the metadata can not depend on the value of a property or method of the model class. (So implementing a Custom ModelMetada Provider wouldn't solve your problem).
Then, you have to find an alternative, hacky, way to do it:
Create a view model with two properties, the original, without the readonly attribute and an additional readonly property, decorated with the readonly attribute.
In the view, decide which of the two to show.
public class MyModel
{
public DateTime MyProperty { get; set;}
[ReadOnly]
public DateTime MyPropertyRo { get; set;}
}
If you want to recover the posted values, the editable version should use the original property in the Telerik control. The non-editable version should use the readonly property in the Telerik control, and the original property in a hidden-field, so that you can recover it in the post.
#if (Model.MyPropertyIsReadOnly)
{
#Html.HiddenFor(m => m.Property)
#Html.TelerikEditorFor(m => m.PropertyRo ...)
}
else
{
#Html.TelerikEditorFor(m => m.Property ...)
}
If you have to do this in many different views, you can create an Html helper (extension method for Html), which receives the 3 properties and includes the last sample code.
Finally, it would be even better to make a custom Editor template, but that's much harder to do if you don't have experience.
There is still another option: contact telerik, and ask them to implement a version of their control which receives a readonly parameter, and does this automatically for you. I think it must be really easy for them to implement it. So, if you're lucky enough...

How can I retrieve the instance of an attribute's associated object?

I'm writing a PropertiesMustMatch validation attribute that can take a string property name as a parameter. I'd like it to find the corresponding property by name on that object and do a basic equality comparison. What's the best way to access this through reflection?
Also, I checked out the Validation application block in the Enterprise Library and decided its PropertyComparisonValidator was way too intense for what we need.
UPDATE: For further clarification (to provide some context), the goal is simply validation that enforces field matching (e.g., password verification). We'd like it to work with property-level attribute data annotations that inherit from the ValidationAttribute class, if possible.
UPDATE: In case anyone is curious, I ended up solving the actual business problem through tweaking code provided as an answer to this question
You can't, basically. The code that checks the object for the presence of the attribute must also assume responsibility for telling any code which type/object it was looking at. You can't obtain any additional metadata from within an attribute.
You cannot do that. See also this question. Try to change the logic to work with the object, checking its attributes, not vice versa. You can also provide more information about your task, not just this narrow question.
You can something like this.
//target class
public class SomeClass{
[CustomRequired(ErrorMessage = "{0} is required", ProperytName = "DisplayName")]
public string Link { get; set; }
public string DisplayName { get; set; }
}
//custom attribute
public class CustomRequiredAttribute : RequiredAttribute, IClientValidatable
{
public string ProperytName { get; set; }
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var propertyValue = "Value";
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForProperties(context.Controller.ViewData.Model, context.Controller.ViewData.Model.GetType());
var property = parentMetaData.FirstOrDefault(p => p.PropertyName == ProperytName);
if (property != null)
propertyValue = property.Model.ToString();
yield return new ModelClientValidationRule
{
ErrorMessage = string.Format(ErrorMessage, propertyValue),
ValidationType = "required"
};
}
}

Categories

Resources