Wednesday, November 28, 2018

Collection constructor with collection initialzer

You can pass anything that is a valid constructor parameter. For example, List permits construction from any enumerable, so you can do this:
string[] ab = new string[] { "a", "b" };
List<string> abcd = new List<string>(ab) { "c", "d" };
// abcd has four elements: "a" "b" "c" and "d"
// Resulting list is { 1, 2, 3, 4, 5 }
var list2 = new List<int>(list) { 4, 5 };
// Resulting list is { 4, 2, 3 }
var list3 = new List<int>(list) { [0] = 4 };
string[] ab = new string[] { "a", "b" };
List<string> abcd = new List<string>(ab) { "c", "d" };
// abcd has four elements: "a" "b" "c" and "d"

this combination notation is useful if you want to clone an existing collection and then make some tweaks to it.

Wednesday, November 14, 2018

Real world Use Case Linq Aggregation

Problem statement

We want to process numbers and categorized them into categories. This could be any data and we want to classify them based on requirement. Just to create small proof of concepts here is the sequence of numbers and idea is to classify them into even, odds and so on.

Solution Statement

Linq Aggregate with Union to show them all.

Class Diagram


Implementation Details

Number Class


Enum NumberType 

NumberGroup Classes




INumberGroup



NumberFilter Class



Main Calling Function
This is it!





Wednesday, September 19, 2018

OneNote Immersive reader Microsoft learning tool

It is one of the amazing tool or add ins for OneNote users. This is very helpful for students and content author to proof reads or improve their language skills.

Long Story short.

Install this plug-ins free of charge if you are OneNote user.

https://www.onenote.com/learningtools


Thursday, May 17, 2018

Azure AppInsight Custom Logs Alerts at very granular level with queries

Introduction

An alert service has become more powerful day by day due to advent of Azure AppInsight. Microsoft is actually listening to developer and support/operation  aka (devops) team very closely. Now we can get alert at very granular level. Whenever you performance test or UAT testing it is good to enable sitecore logs trigger straight into your mail box or into slack or any web hooks. It is easy done. I keep this demonstration simple . I injected simple trace sitecore logs query which will be triggered every 10 mins if error count greater than 1.

Open your Applnsight blade, look for Alert and add new alert . Below screenshot is self explanatory.

Cool part about it you can add any level of AppInsight Analytics query in it. It is very flexible.

E.g
traces
| where message contains "ERROR"






Monday, April 9, 2018

Custom Attribute ValidateApplicationPermission 403 Access Denied Leak and MVC filters

This is always the case when we write some custom filter without being understanding the sequence which we should be using.

One classic example is to handle custom Unauthorized Access for application permissions at Controller Action level. Say if it user has authorization of application but some of the permission is not allowed and still it executes controller-action then it is a serious issue.

[Authorize]
SomeController
{
      [ValidateApplicationPermission]
      SomeAction
    {

    }
}

Problem Definition: 

ValidateApplicationPermission results into 403 access denied leak. It captures access denied and tries to redirect however it is redirecting with executing existing action filter which it should not suppose to be.

For. Eg
Using something like this, here we are trying to use filterContext with HttpContext
filterContext.HttpContext.Server.TransferRequest(Entities.Constants.Entities.Content.Something.Home._403.Path
                            .GetSitecoreItemUrlPath());  

Solution

Use filterContext with its Result, obey filters and its context.
filterContext.Result = new RedirectResult(Entities.Constants.Entities.Content.Something.Home._403.Path.GetSitecoreItemUrlPath());


Implementation Code Base

[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class ValidateApplicationPermission : FilterAttribute, IAuthorizationFilter
{
public enum UnauthorizeResponses
{
RedirectTo403,
EmptyResult
}
private ApplicationPermissions RequiredPermission { get; }
private UnauthorizeResponses UnauthorisedResponse { get; }
public ValidateApplicationPermission(ApplicationPermissions requiredPermission, UnauthorizeResponses unauthorizeResponse = UnauthorizeResponses.RedirectTo403)
{
RequiredPermission = requiredPermission;
UnauthorisedResponse = unauthorizeResponse;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var isAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;
if (!isAuthenticated)
filterContext.Result = new RedirectResult(Entities.Constants.Entities.Content.Something.Home._403.Path.GetSitecoreItemUrlPath());
UserContext userContext;
using (var scope = ServiceLocator.BeginScopeForCurrentSite())
{
var sessionService = scope.Resolve<ISessionService>();
userContext = sessionService.Get<UserContext>(Entities.Infrastructure.Enums.SessionKeys.UserContext);
}
if (userContext == null)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
Sitecore.Diagnostics.Log.Info("Session expired.Redirecting user to login page...", this);
IServiceResponse<string> response = new ServiceResponse<string>();
response.RedirectUrl = Entities.Constants.Entities.Content.Something.Home.Login.Path.GetSitecoreItemUrlPath();
response.Status = ServiceResponseStatuses.Redirect;
filterContext.Result = new JsonResult { Data = new UiServiceResponse<string>(response), JsonRequestBehavior = JsonRequestBehavior.AllowGet };
filterContext.HttpContext.Response.StatusCode = 440;
}
else
{
if (!HttpContext.Current.Response.IsRequestBeingRedirected)
{
Sitecore.Diagnostics.Log.Info("Session expired.Redirecting user to login page...", this);
string returnUrl = WebUtil.AddQueryString(Entities.Constants.Entities.Content.Something.Home.Login.Path.GetSitecoreItemUrlPath(), "returnUrl", Boolean.TrueString.ToLower());
WebUtil.Redirect(returnUrl);
}
}
return;
}
if (!userContext.ValidatePermission(RequiredPermission).HasValue)
{
if (UnauthorisedResponse == UnauthorizeResponses.EmptyResult)
filterContext.Result = new EmptyResult();
else
filterContext.Result = new RedirectResult(Entities.Constants.Entities.Content.Something.Home._403.Path.GetSitecoreItemUrlPath());
}
}
}

Wednesday, April 4, 2018

Awesome-ness of AppInsight Analytics and Failure Preview

The Microsoft Azure AppInsight has made so much easy for us to trace the request execution sequence for any exception traces. You can actually see how the request has been executed for given exception. This way it is very easy to troubleshoot and fine actual root cause of the problem.

Check out for following stuff in Azure AppInsight

  • Failure (Preview)- New feature within AppInsight
  • Operation Id- Locate operation id for any specific exception
  • Open up AppInsight Anaytics query analyzer to view the sequence trace leading to an exception.

 Failure (Preview) Blade

  • Lookup for Count(Failed) vs total request
  • Top 3 Response Codes
  • Top 3 Exception Types
  • Last 24 hours or custom date filter. Slide through graph date statistics. Awesome-ness
  • Operations vs Dependencies vs Exceptions tabs
  • Nevertheless View in Analytics 

Suggested- Click on one of the exception to view Operation ID and from this operation id you get all trails and missing piece of an issue.




End to End transactions

 

Tuesday, March 6, 2018

Sitecore MVC Routing using sitecore pipelines approach

Case Study:

It is very important to separate CMS and CD level MVC routing . When we deploy CD content delivery for any web then it should have very specific routing for the web and most of the cms specific routing should be entrusted to cms solution. It is simple you move all the cms level custom route to custom library and later reference and allow to run at runtime using config patch specific to environment. Like SiteSetting.CMS.Config vs SiteSetting.CD.Config this is will part of continous deployment where it will be picked up during depployment to specific cms and CD environment. By doing this we are separating the responsibility and it help maitain consistent approach specific to cms and cd. This also good for performance of application and it will not conlict with web solution specific to its functionality.

CD with specific to web page
Say http://abc.com/homepage which internally call api via ajax -xhr request say http://abc.com/abc/api/home/Get

CMS with specific to admin or shell page
http://abc.cms/sitecore/admin/api/user this is very specific to cms

Now when you are employing this solution in MVC ensure it is separated for each of this environment. The cms web server will have routing logic loaded at runtime specific to its env without adding extra overload to web CD environment.
Ref:

Implementation Logic

using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using System.Web.Mvc;
using System.Web.Routing;
namespace Abc.Platform.Pipelines.Mvc
{
public class RegisterCustomRoutes
{
public void Process(PipelineArgs args)
{
Log.Info("Sitecore is starting", this);
RegisterRoutes(RouteTable.Routes);
}
public void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"admin controller",
"sitecore/admin/abc/{controller}/{action}",
new { controller = "user", action = "index", id = UrlParameter.Optional }
);
routes.MapRoute(
"admin api",
"sitecore/admin/abc/api/{controller}/{action}",
new { controller = "admin", action = "deleteLogin", id = UrlParameter.Optional }
);
}
}
}
<?xml version="1.0" encoding="utf-8" ?>
<!-- For more information on using transformations
see the web.config examples at http://go.microsoft.com/fwlink/?LinkId=214134. -->
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<sitecore>
<pipelines>
<initialize>
<processor type="Abc.Platform.Pipelines.Mvc.RegisterCustomRoutes, Abc.Platform" patch:after="processor[@type='Sitecore.Pipelines.Loader.ShowVersion, Sitecore.Kernel']" xdt:Transform="Insert"/>
</initialize>
</pipelines>
</sitecore>
</configuration>

Monday, March 5, 2018

Keyvault access :Certificate give permission to Protected item

Introduction

If you are working with keyvault, ensure you import client certificate with proper permission for it to work properly in your development or local machine. This is most common error and you won't be able to figure out the issues unless you create standalone console app to troubleshoot. The keyvault will always fail at this method and it goes in infinite loop.
public async Task<string> GetAccessToken(string authority, string resource, string scope)
       {
           var context = new AuthenticationContext(authority, TokenCache.DefaultShared);
           var result = await context.AcquireTokenAsync(resource, _clientAssertionCert);
           return result.AccessToken;
       }

Stackoverflow

https://stackoverflow.com/questions/34812897/how-to-suppress-an-application-is-requesting-access-to-a-protected-item-popup

Important Note:

Web application may not prompt you with above security pop-up unlike console application.

Fix/Workaround

Run ->type  MMC
Go to your user personal certificate and delete existing certificate that is application specific and import again with below checkbox status in place.





Thursday, March 1, 2018

Calculate distance between two Geo location using Google Map API

Introduction

In order to run this proof of concept , we need google api  key,API_Key This can be obtain as per your personal google account credential. create project in developer console in google and generate api_key for you to create proof of concept to consume google api.

Try Out Proof of concepts: Hosted in JSFiddler

Google Developer Console Login


Create Project in Google API Console

Google Distance Matrix API

Google Autocomplete Location API

Production Implication

Create account for BUPA or contact IS support for the same.
FREE QUOTA

Key Implementation things to note

  • Use new google.maps.DistanceMatrixService(); for recommended route.
  • The below example can be useful to find distance for multiple source and destination location
  • You can limit auto complete or restrict to specific countries.   destinationautocomplete.setComponentRestrictions({
  •     'country': ['aus']
  • Use new google.maps.DirectionsService() to calculate alternative routes with legs and steps directions
  • Property setting optimizeWaypoints: true gives accurate distance remove complexity such as turns and other criteria
  • Property setting provideRouteAlternatives: true, helps you find all route path options
  • It used traveling salesman algorithm for optimal map routing with google maps.->optimizeWaypoints

Code Base Implementation

<div>
<h2>
Orign Location
</h2>
</div>
<div id="locationField">
<input id="originautocomplete" placeholder="Enter your address" onFocus="geolocate()" type="text"></input>
</div>
<div>
<h2>
Destination Location
</h2>
</div>
<div id="locationField">
<input id="destinationautocomplete" placeholder="Enter your address" onFocus="geolocate()" type="text"></input>
</div>
<br>
<div>
<input type='button' value="Find travelled distance" onclick="CalculatedRecommededDistance()"></input>
</div>
<br>
<div>
<strong>Recommended Route Total Distance</strong>
</div>
<div id="outputRecommended"></div>
<div>
<strong>Longeest Route Total Distance</strong>
</div>
<div id="output"></div>
<!-- Replace the value of the key parameter with your own API key. -->
<script src="https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places&callback=initAutocomplete" async defer></script>
// This example displays an address form, using the autocomplete feature
// of the Google Places API to help users fill in the information.
// This example requires the Places library. Include the libraries=places
// parameter when you first load the API. For example:
// <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places">
var placeSearch, originautocomplete;
var componentForm = {
street_number: 'short_name',
route: 'long_name',
locality: 'long_name',
administrative_area_level_1: 'short_name',
country: 'long_name',
postal_code: 'short_name'
};
function initAutocomplete() {
// Create the autocomplete object, restricting the search to geographical
// location types.
originautocomplete = new google.maps.places.Autocomplete(
/** @type {!HTMLInputElement} */
(document.getElementById('originautocomplete')), {
types: ['geocode']
});
// Set initial restrict to the greater list of countries.
originautocomplete.setComponentRestrictions({
'country': ['aus']
});
destinationautocomplete = new google.maps.places.Autocomplete(
(document.getElementById('destinationautocomplete')), {
types: ['geocode']
});
destinationautocomplete.setComponentRestrictions({
'country': ['aus']
});
}
// Bias the autocomplete object to the user's geographical location,
// as supplied by the browser's 'navigator.geolocation' object.
function geolocate() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
var geolocation = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
var circle = new google.maps.Circle({
center: geolocation,
radius: position.coords.accuracy
});
autocomplete.setBounds(circle.getBounds());
});
}
}
function CalculatedRecommededDistance() {
CalculateDistanceforAllAlternativeRoutes();
var origin = document.getElementById('originautocomplete').value;
var destination = document.getElementById('destinationautocomplete').value;
var geocoder = new google.maps.Geocoder();
var service = new google.maps.DistanceMatrixService();
service.getDistanceMatrix({
origins: [origin],
destinations: [destination],
travelMode: 'DRIVING',
unitSystem: google.maps.UnitSystem.METRIC,
avoidHighways: false,
avoidTolls: false,
avoidFerries: false
}, function(response, status) {
var originList = response.originAddresses;
var destinationList = response.destinationAddresses;
var outputDiv = document.getElementById('outputRecommended');
outputDiv.innerHTML = '';
//Display distance recommended value
for (var i = 0; i < originList.length; i++) {
var results = response.rows[i].elements;
for (var j = 0; j < results.length; j++) {
outputDiv.innerHTML += originList[i] + ' to ' + destinationList[j] +
': ' + results[j].distance.text + ' in ' +
results[j].duration.text + '<br>';
}
}
});
}
function CalculateDistanceforAllAlternativeRoutes() {
var directionsService = new google.maps.DirectionsService();
var start = document.getElementById('originautocomplete').value;
var end = document.getElementById('destinationautocomplete').value;
var method = 'DRIVING';
var request = {
origin: start,
destination: end,
travelMode: google.maps.DirectionsTravelMode[method],
provideRouteAlternatives: true,
unitSystem: google.maps.UnitSystem.METRIC,
optimizeWaypoints: true
};
directionsService.route(request, function(response, status) {
var routes = response.routes;
var distances = [];
for (var i = 0; i < routes.length; i++) {
var distance = 0;
for (j = 0; j < routes[i].legs.length; j++) {
distance = parseInt(routes[i].legs[j].distance.value) + parseInt(distance);
//for each 'leg'(route between two waypoints) we get the distance and add it to
}
//Convert into kilometer
distances.push(distance / 1000);
}
//Get all the alternative distances
var maxDistance = distances.sort(function(a, b) {
return a - b;
});
//Display distance having highest value.
var outputDiv = document.getElementById('output');
outputDiv.innerHTML = Math.round(maxDistance[routes.length - 1]) + " KM";
});
}
/* Always set the map height explicitly to define the size of the div
* element that contains the map. */
#map {
height: 100%;
}
/* Optional: Makes the sample page fill the window. */
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
</style><link type="text/css" rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"><style>#locationField,
#controls {
position: relative;
width: 480px;
}
#autocomplete {
position: absolute;
top: 0px;
left: 0px;
width: 99%;
}
.label {
text-align: right;
font-weight: bold;
width: 100px;
color: #303030;
}
#address {
border: 1px solid #000090;
background-color: #f0f0ff;
width: 480px;
padding-right: 2px;
}
#address td {
font-size: 10pt;
}
.field {
width: 99%;
}
.slimField {
width: 80px;
}
.wideField {
width: 200px;
}
#locationField {
height: 20px;
margin-bottom: 2px;
}
view raw view.css hosted with ❤ by GitHub

Tuesday, February 6, 2018

MVC File Upload using Client side and Server Side

Purpose :-

We can achieve file upload either using any of the below options

  • File Upload using html input file field with Jquery Ajax to call server side web api
  • File Upload using MVC Razor Html.BeginForm to call asp.net mvc acction controller or web api
Below code base cover above implementation.


Key technical specifications :-


  1. Enctype =multipart/form-data
  2. List if using out of box asp.net
  3. Request.Form pass through Ajax Jquery or some sort of js library to read file in asp.net
  4. MultiPartFormBoundary 
  5. ContentType MediaTypeHeaderValue("application/octet-stream")

Implementation



<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script>
    $(function () {
        $("#Upload").click(function () {
            var xhr = new XMLHttpRequest();
            var fd = new FormData($(this));
            var files = document.getElementById('fileInput').files;
            for (var i = 0; i < files.length; i++) {
                fd.append("file" + i, document.getElementById('fileInput').files[i]);
            }
            fd.append("title", "File Upload Proof of concepts");
            xhr.open("POST", "/api/SomeController/TestUpload", true);
            xhr.send(fd);
            xhr.addEventListener("load", function (event) {
               
            }, false);
        });
    });
</script>
<div>
       <h1>Attached Supporting Documents</h1>
       
<!--Client side Jquery Ajax file upload option-->
       <fieldset>
           <legend>Please attached your detailed invoice, receipt and certificate as required.</legend>
           <form name="form1" id="frm" method="post" enctype="multipart/form-data">
               <label for="file">Filename:</label>
               <input type="file" id="fileInput" multiple="multiple" />
               <input type='button' id='Upload' value="Upload" />
           </form>
           <legend>You must attach these in either.Jpeg,GIF, PNG or PDF(Maximum 3MB in total) before you can send your claim.</legend>
       </fieldset>
       <!--MVC Razor Server side file upload option 2-->
       @using (Html.BeginRouteForm("myBupa_api", new { controller = "SomeController", action = "TestClaimUpload" }, FormMethod.Post, 
new { enctype = "multipart/form-data" }))
       {
           <span>Select File:</span>
           <input type="file" name="postedFiles" multiple="multiple" />
           <hr />
           <input type="submit" value="Upload" />
           <br />
       }
   </div>
[HttpPost]
      public virtual JsonResult TestUpload(List<System.Web.HttpPostedFileBase> postedFiles)
      {
//Handle above input parameter filecollection if used Html.BeginRouteForm
CancellationToken cancellationToken = default(CancellationToken)
//Handle if call is made through Ajax JQuery
          if (Request.Files != null)
          {
              postedFiles = new List<System.Web.HttpPostedFileBase>();
              for (int i = 0; i < Request.Files.Count; i++)
              {
                  postedFiles.Add(Request.Files[i]);
              }
          }
// Construct URL
       
         var _url = new Uri("http://some web api url to be post to");
        
         // Create HTTP transport objects
         HttpRequestMessage _httpRequest = new HttpRequestMessage();
         HttpResponseMessage _httpResponse = null;
         _httpRequest.Method = new HttpMethod("POST");
         _httpRequest.RequestUri = new Uri(_url);
         // Set if there is any Headers
         _httpRequest.Headers.TryAddWithoutValidation("X-Some-Session-Header", xCorrelationSession);
        
         _httpRequest.Headers.TryAddWithoutValidation("X-Some-Correlation-Request", xCorrelationRequest);
        //Set Custom Header if any key value
         // Serialize Request
         string _requestContent = null;
         var MultiPartFormBoundary = "-----------------------------17599237688550";
         var MultipartContentType = "multipart/form-data";
         MultipartFormDataContent _multiPartContent = new MultipartFormDataContent(MultiPartFormBoundary);
//JsonData may be some form field data
         if (jsonData != null)
         {
             StringContent _jsonData = new StringContent(SafeJsonConvert.SerializeObject(jsonData, this.Client.SerializationSettings).Trim('"'), 
Encoding.UTF8);
             _multiPartContent.Add(_jsonData, "jsonData");
         }
         if (postedFiles!= null)
         {
             foreach (var file in postedFiles)
             {                         
                 StreamContent _file = new StreamContent(file.InputStream);
                 var encodedUploadSafeFileName = WebUtility.UrlEncode(file.FileName).Replace("(", "").Replace(")", "");
                 _file.Headers.Add("Content-Disposition", $"form-data; name=\"Receipts\"; filename={encodedUploadSafeFileName}");
                 _file.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                 _multiPartContent.Add(_file, "attachment", file.FileName);
             }
         }
       
         var multiPartStream = await _multiPartContent.ReadAsStreamAsync();
         var content = new StreamContent(multiPartStream);
            content.Headers.Add("Content-Type", $"{MultipartContentType}; boundary={MultiPartFormBoundary}");
         var progressContent = content;
         _httpRequest.Content = progressContent;
          
cancellationToken.ThrowIfCancellationRequested();
         _httpResponse = await this.Client.HttpClient.SendAsync(_httpRequest, cancellationToken).ConfigureAwait(false);
                 
HttpStatusCode _statusCode = _httpResponse.StatusCode;
         cancellationToken.ThrowIfCancellationRequested();
         string _responseContent = null;
         if ((int)_statusCode == 400)
         {
            var ex = new HttpOperationException(string.Format("Operation returned an invalid status code '{0}'", _statusCode));
             _responseContent = _httpResponse.Content.AsString();
             ex.Response = new HttpResponseMessageWrapper(_httpResponse, _responseContent);
             _httpRequest.Dispose();
             if (_httpResponse != null)
             {
                 _httpResponse.Dispose();
             }
             throw ex;
         }
         if ((int)_statusCode != 200 && (int)_statusCode != 400)
         {
             var ex = new HttpOperationException(string.Format("Operation returned an invalid status code '{0}'", _statusCode));
             ex.Request = new HttpRequestMessageWrapper(_httpRequest, _requestContent);
             _responseContent = _httpResponse.Content.AsString();
             ex.Response = new HttpResponseMessageWrapper(_httpResponse, _responseContent);
            _httpRequest.Dispose();
             if (_httpResponse != null)
             {
                 _httpResponse.Dispose();
             }
             throw ex;
         }
         // Create Result
         var _result = new HttpOperationResponse<SomeObject>();
         _result.Request = _httpRequest;
         _result.Response = _httpResponse;
         // Deserialize Response
         if ((int)_statusCode == 200)
         {
             _responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
             try
             {
                 _result.Body = SafeJsonConvert.DeserializeObject<SomeObject>(_responseContent, this.Client.DeserializationSettings);
             }
             catch (JsonException ex)
             {
                 _httpRequest.Dispose();
                 if (_httpResponse != null)
                 {
                     _httpResponse.Dispose();
                 }
                 throw new SerializationException("Unable to deserialize the response.", _responseContent, ex);
             }
         }
/*         
if (_shouldTrace)
         {
             ServiceClientTracing.Exit(_invocationId, _result);
         }*/
         return _result;
}

Tuesday, January 23, 2018

Dynamically load partial view using mvc action controller


Introduction
As we all know we can have partial views to do many thing. Here is one of the scenario. Say we have different workflow based on operation selection. Either we have static partial views with full pledge workflow embedded in it or you load workflow as partial view dynamically.

 

With Dynamic Approach

  1. We can inject partial view at runtime
  2. The mark up will be lean and light as it will be on-demand load.


For proof of concept I choose existing Asp.net MVC razor from vs.

 

About.cshml

 

@model WebApplication3.Models.FullAndPartialViewModel
@{
    ViewBag.Title = 
"My Master Page";
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script type="text/javascript">
 
    $(document).ready(
function () {
        $(
"p").click(function () {
            
var categoryId = $("#ddlCategory").val();
           

            $(
"#dvCategoryResults").load("/home/InitiateWorkflow", { categoryId: categoryId });
        });
        $(
"#ddlCategory").change(function () {
            
var categoryId = $("#ddlCategory").val();
            $(
"#dvShowSubmissionCriteria").show();

            // $("#dvCategoryResults").load('@(Url.Action("GetCategoryProducts","Home",null, Request.Url.Scheme))?categoryId=' + categoryId);
           
//  $("#dvCategoryResults").load("/home/InitiateWorkflow", { categoryId: categoryId});
        });
    });
</script> 
<div id="container">
 
    
<label for="ddlCategory"><strong>Select a category</strong></label>
    
@Html.DropDownListFor(m => m.CategoryId,
        
new SelectList(Model.CategoryList, "CategoryId""CategoryName", Model.CategoryId), new { id = "ddlCategory", @class = "test" })
    
<br /><br />
    
<div id="dvShowSubmissionCriteria" hidden="hidden">
        
<p>This is submission criteria. Please accept before you proceed</p>
    
</div>
    
<p><strong>Click here to start Workflow -->.</strong></p>
    
<div id="dvCategoryResults">
 
    
</div>
</div>  

 

FullAndPartialViewModel.cs


using System.Collections.Generic;
 
namespace WebApplication3.Models
{
    
public class FullAndPartialViewModel
    {
        
public int CategoryId { getset; }
 
        
public List<CategoryListItem> CategoryList { getset; }
    }
    
public class  CategoryListItem
    {
        
public int CategoryId { getset; }
        
public string CategoryName { getset; }
 
    }
}


HomeController.cs

using System.Web.Mvc;
using WebApplication3.Models;
 
namespace WebApplication3.Controllers
{
    
public class HomeController : Controller
    {
        
public ActionResult Index()
        {
            
return View();
        }
 
        
public ActionResult About()
        {
            ViewBag.Message = 
"Your application description page.";
            
FullAndPartialViewModel viewModel = new FullAndPartialViewModel();
            viewModel.CategoryList = 
new System.Collections.Generic.List<CategoryListItem>();
            viewModel.CategoryList.Add(
new CategoryListItem() { CategoryId = 1, CategoryName = "Service Operation 1" });
            viewModel.CategoryList.Add(
new CategoryListItem() { CategoryId = 2, CategoryName = "Service Operation 2" });
            
return View(viewModel);
        }
       
        
public PartialViewResult InitiateWorkflow(int categoryId)
        {
            
//https://cmatskas.com/update-an-mvc-partial-view-with-ajax/
 
            
var service = new Service();
 
            
if (categoryId == 1)
            {
                service.Name = 
"Workflow 1";
                service.Value = 
"I got 5 Steps!";
                
return PartialView("~/Views/Home/_WF1.cshtml", service);
            }
            
else
            {
                service.Name = 
"Workflow 2";
                service.Value = 
"I got 100 Steps!";
                
return PartialView("~/Views/Home/_WF2.cshtml", service);
            }
        }
    }
}

 

Service.cs

namespace WebApplication3.Models
{
    
public class Service
    {
        
public string Name { getset; }
        
public string Value { getset; }
    }
}


Partial View Workflow 1: _WF1.cshtml

 

@using WebApplication3.Models
@model Service
<p>@Model.Name</p>
<p>@Model.Value</p>

 
Partial View Workflow 2: _WF2.cshtml

 

@using WebApplication3.Models
@model Service
<p>@Model.Name</p>
<p>@Model.Value</p>

 

Output
 

Machine generated alternative text:
Application name Home About 
Select a category Service Operation 2 
Contact 
This is submission criteria. Please accept before you proceed 
Click here to start Workflow 
Workflow 2 
I got 100 Steps! 
C 2018 MY ASRNET Application