I am working with a custom list I've built in SharePoint. I have a feature which has an event handler that kicks off when the feature is activated. That event handler calls (I can debug and watch it execute) the following function:
I'm intending that the function behaves as described here.
private void OrderFields(SPWeb web)
{
// This works fine: I get the expected SPContentType object
// There is only 1 SPContentType in ContentTypes
SPContentType contentType = web.Lists[TASK_LIST_NAME].ContentTypes[0];
contentType.FieldLinks.Reorder(new string[4]
{
"Field1",
"Field2",
"Field3",
"Field4"
});
contentType.Update();
}
The function that calls OrderFields calls web.Update(); as well.
Unfortunately, the reorder call does not reorder my fields on my form. Am I missing something? Can anyone suggest any tricks I might try?
The problem was with my field names.
I had two lookups that I'd programmatically created earlier in the execution of the event handler. These lookups' internal names vary significantly from the "Field1" and "Field2" names I'd given them in the Name and StaticName parameters of my CAML definition.
Instead, the internal name seems to be composed of the Display Name, spaces replaced by "0020", and truncated to a length of 32 characters.
Related
I'm trying to create a more friendly page URL in Kentico with the two letter culture code (en) instead of the full culture code (en-US). I modified the page URL pattern as seen in the image below. Is there a better way of doing this, should I create a custom macro expression?
The other thing I wanted to achieve was having the default culture not in the URL. Maybe I could also use the custom macro expression for it.
I have solved the problem with a custom macro expression. This is used in the URL pattern in combination with the DocumentCulture. The macro expression takes the culture alias name which is in case of the default culture empty and else the two letter code, and returns this with a '/'.
The url pattern for a page:
{%Util.GetUrlCultureCode(DocumentCulture)%}{%NodeAliasPath%}
The macro expression I wrote for fixing this problem is as below.
public class CustomCultureMacroExpression : MacroMethodContainer
{
[MacroMethod(typeof(List<string>), "Returns the url culture code of current document", 1)]
[MacroMethodParam(0, "Culture", typeof(string), "Current culture.")]
public static object GetUrlCultureCode(EvaluationContext context, params object[] parameters)
{
// Branches according to the number of the method's parameters
switch (parameters.Length)
{
case 1:
return GetCultureAliasOfCulture(parameters[0].ToString());
case 2:
// Weird bug causing macro expression in url pattern to have two parameters where the first parameter is null.
return GetCultureAliasOfCulture(parameters[1].ToString());
default:
// No other overloads are supported
return string.Empty;
}
}
private static string GetCultureAliasOfCulture(string cultureCode)
{
var culture = CultureInfoProvider.GetCultureInfo(cultureCode);
var culturealias = culture?.CultureAlias ?? string.Empty;
return string.IsNullOrEmpty(culturealias) ? string.Empty : $"/{culturealias}";
}
}
You may try to update culture codes in Localization -> Cultures, however I'm not sure if it won't have any side effect
If you use the Dynamic Routing module, you can hook into the GetCulture and GetPage global events to do what you are looking for.
For the GetCulture, you would check if the URL has a 2 letter culture code, if it does then use that as the culture.
You can also adjust the GetPage if needed, although the URL slugs generated should work properly, unless you want a fall back that on the GetPage.After, if there is no found page, you can try removing any culture from the URL and looking up the page by that.
I recommended using the NodeAliasPath for the remaining part of the UrlPattern for ease of use.
The asp-items Razor "TagHelper" will add an <option> to a <select> for each value in the SelectList. I want to modify each of those children.
Specifically I want to disable some of them (i.e. add disabled="disabled").
Even more specifically I want to dynamically disable some of them; I'm using angular so I could ng-disabled="{dynamic_boolean_which_determines_disabled}". This means the option could be disabled at first, but after user makes a change, the option could be disabled (without page reload). Angular should take care of this; I think Angular and TagHelpers should work together in theory...
I expected:
I could somehow access an IEnumerable of the children <option> tags that would be created (i.e. one for each item in the SelectList), iterate the children tags, and SetAttribute("disabled") or SetAttribute("ng-disabled")...
I tried:
Creating my own TagHelper which targets the select[asp-items], and tries to GetChildContentAsync() and/or SetContent to reach an IEnumerable <option> tags and iterate them and process each, but I think this will only let me modify the entire InnerHtml as a string; feels hacky to do a String.replace, but I could do it if that's my only option? i.e. ChildrenContent.Replace("<option", "<option disabled=\"...\"")
Creating my own TagHelper which targets the option elements that are children of the select[asp-items], so I can individually process each. This works, but not on the dynamically-added <option> created by asp-items, it only works on "literal" <option> tags that I actually put into my cshtml markup.
I think this'll work but not ideal:
As I said above, I think I can get the result of TagHelper's dynamic asp-items <option></option> <option></option>, as a string, and do a string replace, but I prefer not to work with strings directly...
I suspect (I haven't tried it) that I could just do the work of asp-items myself; i.e. custom-items. But then I'm recreating the wheel by re-doing the work which asp-items could've done for me?
So I hadn't yet read the "AutoLinkHttpTagHelper" in the example which uses string replacement (specifically RegEx replace) to replace every occurrence of a URL, with an <a> pointed at that URL. The cases are slightly different*, but...
Anyway, here's my solution once I learned to stop worrying and love the string modification:
[HtmlTargetElement("select", Attributes = "asp-items")]
public class AspItemsNgDisabledTagHelper : SelectTagHelper
{
//Need it to process *after* the SelectTagHelper
public override int Order { get; } = int.MaxValue;
//https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring#ProcessAsync
public AspItemsNgDisabledTagHelper(IHtmlGenerator gen) : base(gen) {}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
//Notice I'm getting the PostContent;
//SelectTagHelper leaves its literal content (i.e. in your CSHTML, if there is any) alone ; that's Content
//it only **appends** new options specified; that's PostContent
//Makes sense, but I still wasn't expecting it
var generated_options = output.PostContent.GetContent();
//Note you do NOT need to extend SelectTagHelper as I've done here
//I only did it to take advantage of the asp-for property, to get its Name, so I could pass that to the angular function
var select_for = this.For.Name;
//The heart of the processing is a Regex.Replace, just like
//their example https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring#inspecting-and-retrieving-child-content
var ng_disabled_generated_options = Regex.Replace(
generated_options,
"<option value=\"(\\w+)\">",
$"<option value=\"$1\" ng-disabled=\"is_disabled('{select_for}', '$1')\">");
//Finally, you Set your modified Content
output.PostContent.SetHtmlContent(ng_disabled_generated_options);
}
}
Few learning opportunities:
Was thinking I'd find AspForTagHelper and AspItemsTagHelper, (angular background suggested that the corresponding attributes; asp-for and asp-items, would be separate "directives" aka TagHelper).
In fact, TagHelper "matching" focuses on the element name (unlike angular which can match element name... attribute... class... CSS selector)
Therefore I found what I was looking for in SelectTagHelper, which has For and Items as properties. Makes sense.
As I said above, I extend SelectTagHelper, but that's not necessary to answer my original question. It's only necessary if you want access to the this.For.Name as I've done, but there may even be a way around that (i.e. re-bind its own For property here?)
I got on a distraction thinking I would need to override the SelectTagHelper's behavior to achieve my goals; i.e. Object-Oriented Thinking. In fact, even if I did extend SelectTagHelper, that doesn't stop a separate instance of the base SelectTagHelper from matching and processing the element. In other words, element processing happens in a pipeline.
This explains why extending and calling base.Process(), will result in Select doing its job twice; once when your instance matches, and again when the base instance matched.
(I suppose could've prevented SelectTagHelper from matching by creating a new element name like <asp-items-select>? But, not necessary... I just avoid calling base.Process(). So unless that's a bad practice...)
*Different in this way:
They want to create a tag where none exists, whereas I want to add an attribute a tag which is already there; i.e. the <option>
Though the <option> "tag" is generated by the SelectTagHelper in its PostContent (was expecting to find it in Content), and I don't think tags-generated-in-strings-by-content-mods can be matched with their corresponding TagHelper -- so maybe we really are the same in that we're just dealing with plain old strings
Their "data" aka "model" is implied in the text itself; they find a URL and that URL string becomes a unit of meaning they use. In my case, there is an explicit class for Modeling; the SelectList (<select>) which consists of some SelectListItem (<option>) -- but that class doesn't help me either.
That class only gives me attributes like public bool Disabled (remember, this isn't sufficient for me because the value of disabled could change to true or false within browser; i.e. client-side only), and public SelectListGroup Group -- certainly nothing as nonstandard as ng-disabled, nor a "catch-all" property like Attributes which could let me put arbitrary attributes (ng-disabled or anything else) in there.
Trying to exclude properties from a model from being included during serialization.
I am using the following syntax:
JsConfig<MyTestClass>.ExcludePropertyNames = new[] { "ShortDescription" };
Just after that I have the following:
return (from o in __someProvider.GetAll() select (new
{
o.Name,
o.ShortDescription
o.InsertDate
}).TranslateTo<MyTestClass>()).ToList()
However once result is returned from the method, it still contains "ShortDescription" field in the Json. Am I doing something wrong?
JsConfig<T>.ExcludePropertyNames appears to be checked only once for each type, in a static constructor for TypeConfig<T>. Thus, if you are configuring ExcludePropertyNames in your service class, just before returning your response, it might be too late -- the TypeConfig properties may already be set up and cached for MyTestClass. I was able to reproduce this.
A more reliable alternative is to move all of your JsConfig<T> configuration to your AppHost setup code.
If you really do need to do this in your service class, e.g. if you are only conditionally excluding property names, then an alternative approach would be to ensure that JsConfig.IncludeNullValues is false (I believe it is by default) and in your service code set ShortDescription to null when appropriate.
After spending a year working with the Microsoft.Xrm.Sdk namespace, I just discovered yesterday the Entity.FormattedValues property contains the text value for Entity specific (ie Local) Option Set texts.
The reason I didn't discover it before, is there is no early bound method of getting the value. i.e. entity.new_myOptionSet is of type OptionSetValue which only contains the int value. You have to call entity.FormattedValues["new_myoptionset"] to get the string text value of the OptionSetValue.
Therefore, I'd like to get the crmsrvcutil to auto-generate a text property for local option sets. i.e. Along with Entity.new_myOptionSet being generated as it currently does, Entity.new_myOptionSetText would be generated as well.
I've looked into the Microsoft.Crm.Services.Utility.ICodeGenerationService, but that looks like it is mostly for specifying what CodeGenerationType something should be...
Is there a way supported way using CrmServiceUtil to add these properties, or am I better off writing a custom app that I can run that can generate these properties as a partial class to the auto-generated ones?
Edit - Example of the code that I would like to be generated
Currently, whenever I need to access the text value of a OptionSetValue, I use this code:
var textValue = OptionSetCache.GetText(service, entity, e => e.New_MyOptionSet);
The option set cache will use the entity.LogicalName, and the property expression to determine the name of the option set that I'm asking for. It will then query the SDK using the RetrieveAttriubteRequest, to get a list of the option set int and text values, which it then caches so it doesn't have to hit CRM again. It then looks up the int value of the New_MyOptionSet of the entity and cross references it with the cached list, to get the text value of the OptionSet.
Instead of doing all of that, I can just do this (assuming that the entity has been retrieved from the server, and not just populated client side):
var textValue = entity.FormattedValues["new_myoptionset"];
but the "new_myoptionset" is no longer early bound. I would like the early bound entity classes that gets generated to also generate an extra "Text" property for OptionSetValue properties that calls the above line, so my entity would have this added to it:
public string New_MyOptionSetText {
return this.GetFormattedAttributeValue("new_myoptionset"); // this is a protected method on the Entity class itself...
}
Could you utilize the CrmServiceUtil extension that will generate enums for your OptionSets and then add your new_myOptionSetText property to a partial class that compares the int value to the enums and returns the enum string
Again, I think specifically for this case, getting CrmSvcUtil.exe to generate the code you want is a great idea, but more generally, you can access the property name via reflection using an approach similar to the accepted answer # workarounds for nameof() operator in C#: typesafe databinding.
var textValue = entity.FormattedValues["new_myoptionset"];
// becomes
var textValue = entity.FormattedValues
[
// renamed the class from Nameof to NameOf
NameOf(Xrm.MyEntity).Property(x => x.new_MyOptionSet).ToLower()
];
The latest version of the CRM Early Bound Generator includes a Fields struct that that contains the field names. This allows accessing the FormattedValues to be as simple as this:
var textValue = entity.FormattedValues[MyEntity.Fields.new_MyOptionSet];
You could create a new property via an interface for the CrmSvcUtil, but that's a lot of work for a fairly simple call, and I don't think it justifies creating additional properties.
I can't seem to store additional data in a separate contentpart attached to User. I have done the following:
Created a module
In the module I created a Model for ProfilePart and ProfilePartRecord
In the migration I created a table for ProfilePartRecord (from type ContentPartRecord)
In the migration I altered the typedefinition for User, by setting WithPart ProfilePart
I created a driver class, that has 2 edit methods, one for get and one for post (code snippets are below
I also created a handler that adds a storage filter for profilePartRepository of type ProfilePartRecord
Module Structure
Drivers/ProfilePartDriver.cs
Handlers/ProfileHandler.cs
Models/ProfilePart.cs
Models/ProfilePartRecord.cs
Views/EditorTemplates/Parts/profile.cshtml
Migrations.cs
Placement.info
Since I think the issue is in the Driver. This is my code:
Is it going wrong because the part is attached to User? Or am I missing something else.
public class ProfilePartDriver:ContentPartDriver
{
protected override string Prefix
{
get { return "Profile"; }
}
//GET
protected override DriverResult Editor(ProfilePart part, dynamic shapeHelper)
{
return ContentShape("Parts_Profile_Edit", () =>
shapeHelper.EditorTemplate(TemplateName: "Parts/Profile", Model: part, Prefix: Prefix));
}
//POST
protected override DriverResult Editor(ProfilePart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
}
I have used Skywalker's blog. There is one chapter about registering customers by using the User and adding your own content parts to it. Worked nice for me.
First of all - is your ProfilePart editor shown at all when you go to Dashboard and edit a given user? I noticed you're using Parts_Profile_Edit as a shape key, but actually use EditorTemplates/Parts/Profile.cshtml as a template. It's perfectly correct, but note that Placement.info file uses shape keys, so you have to use Parts_Profile_Edit as a shape name in there. Otherwise it won't get displayed.
Second - have you tried debugging to see if the second driver Editor method (the one for handling POST) is being called at all?
Like Bertrand suggested, I'd look into one of the existing modules that work (afaik there is one for user profile in the Gallery) and see the difference. It might be something small, eg. a typo.