Friday, March 10, 2017

I’ve Been Doing Plugin Parameters Wrong For 7 Years

TLDR: Use request/response classes to get typed parameters in your plugins, and share your knowledge!

I’ve been an active Stack Overflow (SO) user for about 7 years now.  There are 3 ways to increase your knowledge from SO.
  1. Being able to ask a question and get an answer in hours.  It’s not always the correct, or the answer you want, but more often than not, it is a great way to increase your knowledge.  This though, is not the primary conduit to increasing your knowledge, that privilege belongs to way #2.
  2. Finding the answers to the questions that have already been asked, and instantaneously getting the knowledge you want (sometimes by the former, “smarter” you).  This is by far the most common method of learning on SO.  It also brings up a new not quite as beneficial coding paradigms (Stack-Overflow-Copy-and-Paste-Yourself-to-Victory).  If this second option is the bread and butter of learning for developers today, then third option is the dessert, the unexpected delightfulness that makes the SO community a great place to contribute to, and learn from.
  3. Providing an answer to a question that you believe to be 100% correct, but then having someone else respond with an even better/more awesome/even more “correcter” answer, to which you tweet about, and then someone else responds with an even better/more awesome/even more “correcter”/more ludicrous answer.  This third option is the subject of today’s post.
Here is how this particular instance of #3 went down…

  • I posted an answer to a SO question about plugin parameters almost 6 months ago (Feel free to refer to the SO question mentioned here: How to know what InputParameters values are possible in Dynamics CRM Plugin context?).  
  • Then Federico Jousset commented on my answer with a helper web page that he has created that is a better tool, IMHO, to answer the OP’s question.  
  • I then tweeted about it (since this knowledge should be shared among the global Dynamics 365 community).
  • Martin Tange responded with a blog post (which I’m assuming he wrote in direct response to my tweet) that brought to light how I’ve been doing plugin parameters wrong since day one.
  • And now you're here learning about it as well!

The MSDN documented approach to accessing parameters is to use “magic” strings and assumed casting
// The InputParameters collection contains all the data passed in the message request.
if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is EntityReference)
{
    // Obtain the target entity from the input parameters.
    EntityReference entity = (EntityReference)context.InputParameters["Target"];
}
Martin’s technique is so simple, I can't believe I haven't seen it used before:
var createReq = new CreateRequest { Parameters = context.InputParameters };
createReq.Target; // Is typed as Entity vs EntityReference (DeleteRequest)
To which I’ve wrapped in an extension method to help remove some additional key strokes (which I’ve added to the DLaB.Xrm.Plugin namespace):
/// <summary>
/// Populates a local version of the request using the parameters from the context.  This exposes (most of) the parameters of that particular request
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="context">The context.</param>
/// <returns></returns>
public static T GetRequestParameters<T>(this IPluginExecutionContext context) where T: OrganizationRequest
{
    var request = Activator.CreateInstance<T>();
    request.Parameters = context.InputParameters;
    return request;
}
 
/// <summary>
/// Populates a local version of the response using the parameters from the context.  This exposes (most of) the parameters of that particular response
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="context">The context.</param>
/// <returns></returns>
public static T GetResponseParameters<T>(this IPluginExecutionContext context) where T : OrganizationResponse
{
    var response = Activator.CreateInstance<T>();
    response.Results = context.OutputParameters;
    return response;
}
So now, this is even shorter:
var parameters = context.GetRequestParameters<CreateRequest>();
parameters.Target;
So my challenge to you dear reader, get involved in your community.  You may never know when an attempt to share your knowledge with someone else, ends up unexpectedly resulting in others sharing their knowledge with you!

Friday, November 11, 2016

Learn All the Shortcuts!!!

 

I just installed Mads Kristensen’s Learn the Shortcut extension for Visual Studio.  Within the first 5 minutes, it has already paid for itself (yes it’s free, but it does cost some time to download, install, and restart VS).  

The extension is fairly simple, each time you do something that there exists a shortcut for, it outputs it to a custom output window.  I’ve been wanting to learn the CollapsetoDefinitions shortcut for a while, I just always forget, so I ran that one first, and continued on with my day, hoping that if I do it a few more times, maybe I’ll learn it. 

But then… it showed a shortcut for “RunAllTests”.  I didn’t even think to look for a shortcut for running all tests, even though I navigate to the test explorer and click the “Run All” button multiple times a day.

RunAllShortCut

So not only is the tool great for the shortcuts you know you want to start learning, but just need a little extra help with, it’s also great for those shortcuts that you didn’t even think about possibly having a shortcut for.  Figuring out what you don’t know you don’t know for the win!

LearnAllTheShortCutzzz

Saturday, September 3, 2016

An Evening with Me and #XRMToolbox Development

Image result for SocialI’ve got a couple GitHub issues on for my XrmToolBox Plugins that have been gnawing at me for a while.  In an effort to have a more enjoyable time, I thought I’d attempt to do a social programming event.  This will be unscripted event, open to anyone.  There will be 0 Slides, and 0 prep work.  It will offer people an opportunity to ask questions, see what’s involved with XTB development, and just generally hang out, with an underlying theme of attempting to knock out as many GitHub issues as I can.  It could be fun!  On the other hand, it could be incredibly boring.  Tons of people could log in!  On the other hand maybe no one will show up.  Whatever the case, we won’t know until we try!

  • Who: Myself and anyone that wants to join.  
  • What: Social programming (if that’s not already I thing, I’m making it one now!)  Feel free to un-mute, ask questions, make suggestions, or tell me I'm missing a semi-colon!
  • Where: Skype For Business Link.
  • When: September 8, 2016 from 9pm to midnight EDT
  • Why:  I’ve got stuff I want to do, and thought this could be a fun-er way of doing it! :-)

Friday, September 2, 2016

Allow Native Mapping When Referencing a Parent Entity

Normal Out of Box (OOB) CRM Relationship mapping can be extremely beneficial, but it has a very big “gotcha”.  They only work when creating a child entity from the parent form.  So if the user is on the Parent Entity Form, and adding a new entity via a Grid, then the mapping will automatically be populated for the user.  But what happens if you have an entity that is serving as a M:M entity (think Lead Products).   Generally, a user would create a Lead Product from the Lead Form.  This means if any mappings are setup from the Product to the Lead Product, they will not be applied.

There is no way for this to get applied natively, since the mapping takes place before the Lead Product is loaded, but the user doesn’t select a Product until after the Lead Product is loaded.  There is however, a simple partial work around.  Create a plugin that performs the mapping using what is defined in the OOB Field Mapping for the Relationship.  For extensibility, define the parent field(s) in the plugin step.  Below is a working example of such a plugin (utilizing the DLaB.Xrm.2016 NuGet Package).
public override void RegisterEvents()
{
    RegisteredEvents.AddRange(new RegisteredEventBuilder(PipelineStage.PreOperation, MessageType.Create).Build());
}
 
protected override void ExecuteInternal(ExtendedPluginContext context)
{
    if (UnsecureConfig == null)
    {
        context.Trace("No Fields listed in the Unsecure Configuration.  Nothing from which to initialize the entity!");
        return;
    }
    var target = context.GetTarget<Entity>();
    foreach (var field in UnsecureConfig.Split(new [] {",","|",Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries))
    {
        InitializeFromField(context, target, field);
    }
}
 
/// <summary>
/// Loads the configured mappings for the entity from the given field.  
/// Only attributes that do not exist in the target are set, and only if the field contains an EntityReference in the target
/// </summary>
/// <param name="context">The context.</param>
/// <param name="target">The target.</param>
/// <param name="field">The field.</param>
private void InitializeFromField(ExtendedPluginContext context, Entity target, string field)
{
    context.TraceFormat("Initializing from field {0}", field);
    var parent = target.GetAttributeValue<EntityReference>(field);
    if (parent == null)
    {
        context.TraceFormat("No Parent found for field {0}", field);
        return;
    }
    var mappedEntity = context.SystemOrganizationService.InitializeFrom(parent, target.LogicalName, TargetFieldType.ValidForCreate);
    foreach (var att in mappedEntity.Attributes.Where(a => !target.ContainsAllNonNull(a.Key)))
    {
        context.TraceFormat("Adding attribute {0}:{1}", att.Key, att.Value);
        target.Attributes.Add(att);
    }
}

The Details

  • The Plugin must be registered for each entity to be initialized
  • The Unsecure Configuration Settings must contain a comma (or pipe or newline) delimited list of parent lookup attributes to initialize the entity from. i.e. “ProductId”
  • The parent field lookup field must be populated on the initial save.
  • Only un-populated fields will be mapped.  If a user has defined a value already, it will not be overridden.
  • An end user will not see the mapped values on the form until after the initial save refresh occurs.
That’s it.  A few lines of code, and now your Entity Relations Field Mappings can be used in many more places!

Thursday, August 11, 2016

How to Add a Spacer to a CRM Business Process Flow

The CRM form designer has a Spacer feature, that allows for having blank spaces where normally fields are required.  This feature is sorely missing in CRM Business Process Flows, but the good the news is that you can implement your own space with no coding required!  Just to be clear about what I’m talking about, here is a picture, showing how the Sub-Total should be over the Tax Amount and Total Amount:
BPF
This requires two simple steps:
  1. Add a field to the BPF where the space is desired, that doesn’t and exist in the BPF and will never need to be displayed anywhere on the form,  I’ll use “Created By (Delegate)” in this example, but you could always create your own:
    BPF-Designer
  2. Create a business rule that always hides the field:
    image

Now, when the form loads, the Business Rule will hide the field, allowing it to serve as a spacer:
BPF-Blank

Monday, August 8, 2016

Pick a Mountain, Any Mountain

For just being larger than average piles of dirt and rock, mountains can do and cause extraordinary things in an individual’s life.  Last year, in celebration of our 10 year anniversary, my wife and I decided to climb Pike’s Peak.  It’s a 13 mile journey from Manitou Springs, CO, rising up 7800 feet, past the tree line, half way to the stratosphere.  It’s a hike made difficult by its length, incline, and diminished levels of oxygen (especially for someone from oxygen rich Indiana) at the top of the 14,118 foot mountain.  So why would anyone want to climb it?  My answer was the same as the standard answer pertaining to mountains, “Because it’s there.”
Distant Pikes Peak
Pike’s Peak posed a challenge for me.  It forced me to face something I had never done before, with no guarantee that I could even accomplish it.  Once the goal was set, it required my wife and I to make plans, buy tickets, comprise arrangements for the kids, and prepare for the challenge months before hand.  For months, we consumed and were consumed by everything we had available to us about the journey, multiple blog articles/videos about the journey, where to rest, where to get water, what to wear, what to bring, where to park, and what potential emergencies to prepare for.  We learned from other’s mistakes, and envied those that had already completed the journey.

When we arrived in Manitou Springs, we were simultaneously excited and daunted by the shear size of the mountain.  We met first hand, both those that had made the trek, and those that had lived in Colorado their whole lives, and only ever drove to the peak, eagerly anticipating that we would be included in the former group, rather than the later.

The morning of the hike, we left our hotel at 3 am and arrived at the base, in near total darkness, turned on our flashlights, and started climbing.   We were making great time and arrived at Barr Camp, slightly ahead of schedule.  After a quick bite to eat and some water to drink, we continued our ascent.  Things continued to go well until after we passed the tree line.   The unseen diminished oxygen levels started to slowly take its toll.  There had been few rests/stops up until that point, but shortly thereafter, I started requiring more and more breaks to catch my breath.  The final 2 hours of the climb saw me stopping every 10-40 feet to catch my breath, and sitting down every 200-400 feet in an attempt to regain the energy/strength in order to continue forward. 

It was at this point, when I was the closet to accomplishing the goal I had set out for myself months before hand, that I began to seriously question whether I would be able to finish.  It was at this point, that my wife stopped becoming a companion on the trip, and started to become a slave driver cheerleader.  At this point in time, I should probably explain some of the physical differences between my wife and I.  My wife is an avid workout enthusiast.  She exercises most days of the week, eats healthy, and runs half-marathons (3 of which at 4, 5 and 7 months of pregnancy).  Let’s just say, she is a healthy in shape woman.  I on the other hand, managed to lose 10 pounds before the trip, but rarely work out more than twice a week, and besides a single 5K, haven’t ran a race since high school.  It is my firm belief that the last mile of the climb, she could have ran up and down 3 times, before I made it up once.  But instead, she continued to encourage and coach me up the rest of the mountain.

When we finally reached the top, I was met a rather unusual combination of emotions.  I was tired, exhausted, sore and weak.  On the other hand, I was filled with an incredible sense of accomplishment and pride, having overcome an extremely challenging task.  If my goal was to drive up the mountain, I would have been able to enjoy the same views, and the same (IMHO) below average donuts that await any visitor to the top.  Because of the difficulty of the goal, the sense of accomplishment was several magnitudes greater.  It was and is, the most difficult physical accomplishment of my life.

The TopAllegient At The Top

Last month I realized a rather large career goal, being bestowed as a Microsoft MVP Award winner.  It, as my Pike’s Peak adventure, was a goal that I had set for myself long before it became realized.  It, as my Pike’s Peak adventure, was only accomplished via the support and encouragement of those around me.  And it, as my Pike’s Peak adventure, was a goal, with no guarantee of success.  I can only attribute my success to lots of late nights and early mornings working to further the CRM community along with the support of my wife, my co-workers, and my company itself.  My wife sacrificed her time with me, allowing/encouraging/supporting me as I spent large amounts of personal time working on open source projects.  I had multiple co-workers that encouraged me to continue working towards being an MVP, and even being the direct result of why I was nominated in the first place.  Allegient, a consulting firm, and my current employer, is a company that sees value in its employee’s spending time outside of work doing things that aren’t directly related to a client or billable work.  I’m not asked or expected to consistently work 50 hour weeks, which gives me the spare time I need to contribute to the community as a whole.  They even paid for me to attend conferences like Extreme CRM where I got to meet first hand other CRM MVPs who supported and encouraged me as well.

What are your current long term goals?  What are you working for that has a high uncertainty of success?  Do you have the support from your friends/family/co-workers/company to accomplish them?  What mountain are you climbing?  Pick a mountain, any mountain, and don’t stop until you reach the top!

Tuesday, July 26, 2016

GetClientContext Is Not Defined When Navigating Back To WebResource Html File

Ran into an interesting issue with chrome caching the ClientGlobalContext.js.aspx, and not providing an Xrm context to my javascript, when using the browser to navigate back to my webresource.  So just to clarify:
  1. Hit custom html WebResource via browser.
  2. WebResource loads ClientGlobalContext.js.aspx via script tag:
    <script src="../../ClientGlobalContext.js.aspx" type="text/javascript"></script>
  3. WebResource Displays link to CRM Entity.  Navigate to CRM Entity via link.
  4. Click “back” button in browser to navigate back to the custom WebResource.
  5. Receive JS error:
    GetClientContext is not defined
So, what’s going on?  Opening up the Developer Tools / Network tab on the initial load shows how it’s supposed to be working:


The first line is the load from the custom WebResource.  The second line, is then the ClientGlobalContext.js.aspx, loads itself, but as an xhr, rather than a script.  This loads the Xrm Context.
But when I look at the Network Tab when when I get the error: this is what I see:





The ClientGlobalContext.js.aspx is being loaded from cache.  That’s a good thing normally, but it never actually executed the xhr request.  Why Not?  When I opened the cached document, it was actually the xhr that was cached:


So how do I prevent the xhr from getting cached over the top of the script version?  Just append a query string to the end of the aspx page:
<script src="../../ClientGlobalContext.js.aspx?type=script" type="text/javascript"></script>
That way, the files can get cached separately…