Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

replacement for html.GetUnobtrusiveValidationAttributes html.GetClientValidationRules #5028

Closed
omuleanu opened this issue Jul 15, 2016 · 25 comments

Comments

@omuleanu
Copy link

omuleanu commented Jul 15, 2016

hi
in mvc 5 there was html.GetUnobtrusiveValidationAttributes
in rc1 I think there was html.GetClientValidationRules
now, I can't find anything that will return the same

I'm developing a custom helper and I need to get the client validation attributes

@Eilon
Copy link
Member

Eilon commented Jul 18, 2016

@dougbu ?

@Eilon
Copy link
Member

Eilon commented Jul 18, 2016

@rynowak I think you worked in this area too?

@dougbu
Copy link
Member

dougbu commented Jul 18, 2016

In RC1, DefaultHtmlGenerator had a GetClientValidationRules() method. That class now exposes a AddValidationAttributes() method with a similar purpose. Both methods were / are virtual. But neither are / were exposed through IHtmlHelper.

For this use case, I suggest extending IHtmlHelper with new methods. Those methods can examine / update the TagBuilder that TextBox() and so on return.

public IHtmlContent MyTextBox(
    this IHtmlHelper helper,
    string expression,
    object value,
    string format,
    object htmlAttributes)
{
    var content = helper.TextBox(expression, value, format, htmlAttributes);
    var tagBuilder = content as TagBuilder;
    if (tagBuilder != null)
    {
        tagBuilder.AddCssClass("classy");
    }

    return content;
}

@omuleanu
Copy link
Author

so is there a way now to get the validation attributes ?
imagine I have something like this:

public static IHtmlContent About<T>(this IHtmlHelper helper)
{
   // how to get a Dictionary<string, string> 
   // of html <attribute, value> needed for jquery.validate unobtrusive
}

@dougbu
Copy link
Member

dougbu commented Jul 18, 2016

If looking for the attributes a specific helper would add, could do something like I showed above but with

var dictionary = tagBuilder.Attributes
    .Where(item => item.Key.StartsWith("data-", StringComparison.OrdinalIgnoreCase))
    .ToDictionary(keySelector: item => item.Key, elementSelector: item => item.Value);

Could also subclass DefaultHtmlGenerator e.g.

public class MyHtmlGenerator : DefaultHtmlGenerator
{
    public MyHtmlGenerator(
        IAntiforgery antiforgery,
        IOptions<MvcViewOptions> optionsAccessor,
        IModelMetadataProvider metadataProvider,
        IUrlHelperFactory urlHelperFactory,
        HtmlEncoder htmlEncoder,
        ClientValidatorCache clientValidatorCache)
        : base(
              antiforgery,
              optionsAccessor,
              metadataProvider,
              urlHelperFactory,
              htmlEncoder,
              clientValidatorCache)
    {
    }

    public new void AddValidationAttributes(...)
    {
        // ...
    }
}

Then place MyHtmlGenerator in DI and use it from your extension methods e.g.

var generator = helper.ViewContext.HtmlContext.RequestServices.GetRequiredService<IHtmlGenerator>() as MyHtmlGenerator

Unfortunately ClientValidatorCache was placed it in an .Internal namespace. This implies nothing about it can be relied upon. Might be better to create a wrapper class that delegates to the default IHtmlGenerator but is itself registered without reference to that interface.

@omuleanu
Copy link
Author

omuleanu commented Jul 18, 2016

ok, so the way to get the validation attributes now is to generate an editor (textbox, hidden input), cast it as TagBuilder and after filter the TagBuilder.Attributes, will this get easier in the future ?

@Eilon
Copy link
Member

Eilon commented Jul 19, 2016

I don't believe that inspecting the TagBuilder attributes produced by a random other helper is a recommended approach.

@dougbu can we just add an interface / API that exposes the required information? I don't think any of the examples given here are supported or recommended...

@dougbu
Copy link
Member

dougbu commented Jul 19, 2016

@Eilon sure, we can add an interface that DefaultHtmlGenerator would also implement. We can't make AddValidationAttributes() public but can call that from a new method.

@omuleanu exactly what information do you need? In particular would a new method similar to the existing AddValidationAttributes() be right for your custom helper?

@omuleanu
Copy link
Author

I need to get the collection of js validation attributes, this method was present in mvc5, 4, 3
this how it is defined in mvc 5:

//
// Summary:
//     Gets the collection of unobtrusive JavaScript validation attributes using the
//     specified HTML name attribute and model metadata.
//
// Parameters:
//   name:
//     The HTML name attribute.
//
//   metadata:
//     The model metadata.
//
// Returns:
//     The collection of unobtrusive JavaScript validation attributes.
public IDictionary<string, object> GetUnobtrusiveValidationAttributes(string name, ModelMetadata metadata);

@omuleanu
Copy link
Author

@dougbu is this going to be implemented any time soon ?

@dougbu
Copy link
Member

dougbu commented Jul 28, 2016

@omuleanu I don't know.

@danroth27, @Eilon this doesn't have a milestone and seems to be missing all labels. Suggest this would be an Enhancement.

@Eilon
Copy link
Member

Eilon commented Jul 29, 2016

@dougbu let's close on a design with @rynowak and we can get this in.

dougbu added a commit that referenced this issue Sep 2, 2016
…alidationAttributes()` available

- #5028
- helpers similar to our HTML or tag helpers can use the new singleton to examine validation attributes
 - in the most common case, helpers add validation attributes to a `TagBuilder` but that is not required
- separating the `ValidationAttributesProvider` from `DefaultHtmlGenerator` avoids creating two instances of that singleton
 - would be even uglier to require callers to cast an `IHtmlGenerator` to `IValidationAttributeProvider`
@dougbu
Copy link
Member

dougbu commented Sep 2, 2016

Slight side note @Eilon:

I don't think any of the examples given here are supported or recommended...

Early on I mentioned subclassing DefaultHtmlGenerator. Other than the need to downcast an IHtmlGenerator to DefaultHtmlGenerator to use the newly-public method, the demonstrated approach suffers from the DefaultHtmlGenerator having a constructor parameter from an Internal namespace.

More generally, we force creating a DefaultHtmlGenerator subclass to reference an Internal namespace, despite providing a load of virtual and / or protected methods. Subclassing the ValidationAttributesProvider class I proposed in #5223 (and replacing it in DI) has the same issue.

@ajaybhargavb tells me ClientValidatorCache was placed as it was for consistency w/ FilterCache (now ControllerActionInvokerCache). But no instances of ControllerActionInvokerCache are passed in non-Internal APIs.

Should we move ClientValidatorCache to a non-Internal namespace to clean this up?

@akonyer
Copy link

akonyer commented Sep 7, 2016

So since this is a milestone for 1.1.0, is there a workaround to get access to these validation attributes now?

So far all I can think of is to manually loop through the attributes and check if each validation attribute exist on the property, and handle each individually, which is less than ideal.

@dougbu
Copy link
Member

dougbu commented Sep 7, 2016

OP in #5240 provided his workaround for pre-1.1.0.

dougbu added a commit that referenced this issue Sep 9, 2016
…alidationAttributes()` available

- #5028
- helpers similar to our HTML or tag helpers can use the new singleton to examine validation attributes
 - in the most common case, helpers add validation attributes to a `TagBuilder` but that is not required
- separating the `ValidationAttributesProvider` from `DefaultHtmlGenerator` avoids creating two instances of that singleton
 - would be even uglier to require callers to cast an `IHtmlGenerator` to `IValidationAttributeProvider`
dougbu added a commit that referenced this issue Sep 14, 2016
…s()` available

- #5028
- helpers similar to our HTML or tag helpers can use the new singleton to examine or add validation attributes
 - in the most common case, helpers add validation attributes to a `TagBuilder`
- separate `DefaultValidationHtmlAttributeProvider` from `DefaultHtmlGenerator`
 - avoids creating two instances of the `DefaultHtmlGenerator` singleton
 - would be even uglier to require callers to cast an `IHtmlGenerator` to `ValidationHtmlAttributeProvider`
- `[Obsolete]` old `DefaultHtmlGenerator` constructor
@dougbu
Copy link
Member

dougbu commented Sep 14, 2016

809d2bf

@omuleanu
Copy link
Author

omuleanu commented Sep 14, 2016

hi @dougbu
so what will be the new way of getting the validation attributes (Dictionary<string, object> ?

@dougbu
Copy link
Member

dougbu commented Sep 14, 2016

Get the ValidationHtmlAttributesProvider from DI then do something like

public IHtmlContent MyTextBox(
    string expression,
    object value,
    string format,
    object htmlAttributes)
{
    var tagBuilder = new TagBuilder(...);
    tagBuilder.AddCssClass("classy");

    // Get ModelExplorer and do other custom helper stuff...

    // Add the validation attributes.
    _validationAttributeProvider.AddAndTrackValidationAttributes(
        viewContext,
        modelExplorer,
        expression,
        tagBuilder.Attributes);

    return tagBuilder;
}

@omuleanu
Copy link
Author

omuleanu commented Sep 14, 2016

@dougbu not sure I understand you correctly,
so do I still have to use the TagBuilder workaround ?
( I'm not using the TagBuilder for anything else, I'm just creating a textbox with it, just to get the attributes)

        static IDictionary<string, string> UnobsValid(IHtmlHelper html, string prop, ModelExplorer metadata)
        {
            var content = html.TextBox(prop);
            var tagBuilder = content as TagBuilder;

            return tagBuilder.Attributes
                .Where(item => item.Key.StartsWith("data-", StringComparison.OrdinalIgnoreCase))
                .ToDictionary(item => item.Key, item => item.Value);
        }

can this ^ method be improved now, or it remains the same ?

@dougbu
Copy link
Member

dougbu commented Sep 14, 2016

If you just want the dictionary, the following would work:

var attributes = new Dictionary<string, string>();
_validationAttributeProvider.AddAndTrackValidationAttributes(
    viewContext,
    modelExplorer,
    expression,
    attributes);

return attributes;

If you aren't actually writing out the HTML attributes i.e. the dictionary is just for inspection, use _validationAttributeProvider.AddValidationAttributes() instead.

@jsdmitry
Copy link

Please learn me, how get the ValidationHtmlAttributesProvider from DI?

@omuleanu
Copy link
Author

@dougbu how do I get the _validationAttributeProvider, as you can see I have the IHtmlHelper and ModelExplorer
will it be like this ? :
html.ViewContext.HttpContext.RequestServices.GetService(typeof(IValidationAttributeProvider))

@dougbu
Copy link
Member

dougbu commented Sep 14, 2016

@omuleanu sure, that service locator pattern is a fine fallback if your helper class isn't either itself in DI or activated. In those cases, you can just use constructor injection.

@omuleanu
Copy link
Author

omuleanu commented Sep 14, 2016

@dougbu well the html helper is an extension method on IHtmlHelper so there's no constructor, or there's some new way doing helpers ?

@dougbu
Copy link
Member

dougbu commented Sep 14, 2016

there's some new way doing helpers ?

I doubt we have much guidance in this area but ASP.NET Core MVC offers a number of choices for various helper extension / addition scenarios. My recommendations

  1. If you want to change what a helper e.g. @Html.TextBox() does, subclass DefaultHtmlGenerator and override the relevant method(s) e.g. GenerateTextBox() or the lower-level GenerateInput().
  2. If you want to add a very simple tag helper i.e. one that depends only on what IHtmlHelper exposes, write an IHtmlHelper or IHtmlHelper<TModel> extension method.
  3. If you want to add an unrelated helper, create a new class and place that type in DI. Then use @inject to include a property of the new type in your .cshtml files. (Probably need the class to implement IViewContextAware and its DI registration to be transient.)

The above avoids the service locator pattern. But feel free to use that pattern if the architectural and (likely) performance downsides are 🆗 in your application.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants