2015年10月21日星期三

DNN7 WebApi CamelCase problem

I used to encounter an error with DotNetNuke version 7 several time which displays all modules with a name of "undefined" and a broken picture.



This happens when people try to configure their DNN Web Api library to JSON format. Because in C# this is usually done by PascalCase and in JavaScript this is but done using camelCase. So when people want to consume the service with some javascript library, AngularJS for example, they configure the data source to JSON format. In DNN, it's usually done like this.

public class RouteMapper : IServiceRouteMapper
{
  public void RegisterRoutes(IMapRoute mapRouteManager)
  {
    var formatter = System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    formatter.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

    mapRouteManager.MapHttpRoute("Invoice", "default", "{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional }, namespaces: new[] { "DnnWebApi.Controllers" });
  }
}

This will however configure the data format to be camelcase globally. And DNN has its own Web Api implementations which use actually PascalCase. So camelCase will make its client scripts break.

for (var i = 0; i < moduleList.length; i++) {
  ul.append('<li><div class="ControlBar_ModuleDiv" data-module=' + moduleList[i].ModuleID + '><div class="ModuleLocator_Menu"></div><img src="' + moduleList[i].ModuleImage + '" alt="" /><span>' + moduleList[i].ModuleName + '</span></div></li>');
}

So my solution is pretty simple and dumb. Switch to camelcase when the controller instantiates, and switch it back when it disposes.

public class AppController: DnnApiController, IDisposable
{
  private IContractResolver _resolver;

  public AppController()
  {
    var formatter = System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter;

    _resolver = formatter.SerializerSettings.ContractResolver;
    formatter.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

  }

  //your implementation...

  void IDisposable.Dispose()
  {
    var formatter = System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    formatter.SerializerSettings.ContractResolver = _resolver;            
  }
}

2015年10月17日星期六

Simple sidebar transition

Simple Sidebar Transition

Sidebars are responsive components that display various forms of information to the side of applications or sites. This approach is designed to fit in with the Bootstrap framework. Hence, Bootstrap components and JQuery are needed.

Default Sidebar

Default Sidebar works as same as .col-md-3 in Bootstrap grid system. But when the device width is less than its breakpoint size, it hides itself instead of Collapsing to start.

<div class="container">
  <div class="row">
    <div class="sidebar">
    </div>
    <div class="col-md-10">
    </div>
  </div>
</div>

Fixed to Top

The sidebar hides itself when the device width is less than its breakpoint size. And it can be toogled to show and hide the element via class changes.

Adjustment required

The fixed sidebar will overlay Bootstrap Navbar and create a gap when the screen scrolls down. So the following adjustments are needed.

  • Add .sidebar-fixed and include a .sidebar-container to contain content.
  • Add .navbar-fixed-top to navbar if available.
  • Add offsetting class .col-md-offset-2 to the right side.
  • Add padding to the top of the <body>. By default, the navbar is 50px high.
  • Set up toggle element via data-toogle-target attribute.

Example

<div class="container">
  <div class="row">
    <div class="sidebar sidebar-fixed">
      <button type="button" class="btn" data-toggle-target="#sidebar-fixed-1"><span class="glyphicon glyphicon-align-justify"></span></button>
      <div class="sidebar-container">
    </div>
    <div class="col-md-10 col-md-offset-2">
    </div>
  </div>
</div>

Javascript

Include the following code snippet to your page and replace the toggle element selector with your own. By default, it is .sidebar>.btn It requires JQuery.

+function($){
  function toggleSidebar(e) {
    $($(e.currentTarget).attr('data-toggle-target')).toggleClass('open');
  }

  $(document).on('click.sidebar', '.sidebar>.btn', toggleSidebar);
}(jQuery)

Inverted Fixed to Top

Modify to sidebar to have it collapse regardless deveice width by adding .sidebar-fixed-inverse.

<div class="container">
  <div class="row">
    <div class="sidebar sidebar-fixed-inverse">
      ...
    </div>
  </div>
</div>

Style sidebar

Include the following style to your page or stylesheet file to make it properly display.

.sidebar-fixed, .sidebar-fixed-inverse{
  background-color:#eee;
  position:fixed;
  top:51px;
  bottom:0;
  z-index:2;
  transition: left 0.3s;
  -moz-transition: left 0.3s; /* Firefox 4 */
  -webkit-transition: left 0.3s; /* Safari 和 Chrome */
  -o-transition: left 0.3s; /* Opera */
}
.sidebar>.btn{
  display:none;
  border-radius:0;
  background-color:#eee;
  border-color:#e0e0e0;
}
.sidebar-fixed>.btn, .sidebar-fixed-inverse>.btn{
  position:absolute;
  right:-40px;
  top:0;
 display:block;
}
.sidebar-fixed>.sidebar-container, .sidebar-fixed-inverse>.sidebar-container{
  overflow:auto;
  position:absolute;
  top:0;
  bottom:0;
  margin:0;
  width:100%;
}
.sidebar.open{
  left:0;
}
@media(min-width: 768px){
  .sidebar{
    display:none;
  }
  .sidebar-fixed, .sidebar-fixed-inverse{
    display:block;
    width:25%;
    left:-25%;
  }
}
@media(min-width: 992px){
  .sidebar{
    display:block;
    float:left;
    width:16.66666667%;
  }
  .sidebar-fixed, .sidebar-fixed-inverse{
    width:16.66666667%;
    left:-16.66666667%;
  }
  .sidebar-fixed{
    left:0;
  }
  .sidebar-fixed>.btn{
    display:none;
  }
}

2015年10月4日星期日

UNBLUR BACKGROUND (PART 3)

Fading effect

So the fading effect would be using a loop of timed-out call to simulate. It will be something like this...

var painter = $('<canvas>').attr('width', $(window).width()).attr('height', $(window).height())[0], 
    pctx = painter.getContext('2d'), ctx = $('#canvas2')[0].getContext('2d'), img= $('img')[0];
for(var i = i; i <= 10; i++){
  (function(i){
    setTimeout(function(){
        pctx.strokeStyle = "rgba(0,0,0,"+(1 - i / 10)+")";
        pctx.beginPath();
        pctx.moveTo(760, 20);
        pctx.lineTo(760, 100);
        pctx.stroke();
        ctx.drawImage(img, 0, 0, img.width, img.height);
        ctx.globalCompositeOperation = "destination-in";
        ctx.drawImage(painter, 0, 0);
        ctx.globalCompositeOperation = "source-over";
 },i * 200);
  })(i);
}

But I cannot make it directly work with mousemove event, because it fires for every pixel mouse move which is very costly and inefficient. And also for every line it draws, it draws also one image and one canvas as well, which is also very expensive.

This is what I'm going to do. Firstly, the event, instead of directly drawing, it just records the current mouse coordinate and the time stamp and trigger one recursive function if it's not started. And then the recursive function, which will constantly run once in a short while till no more event occurs, it contains the logic of drawing, coordinate collection adding and removal and the indicator that decide whether it can be triggered. Finally, I need a collection that stores coordinates so that I can draw all of them in the painter and copy the painter to the main canvas.

Collect the coordinates

The first thing is, I'm going to need an array to store coordinate objects.

var points = [];

Handle the mousemove event

Firstly I instantiate several global variables so I can read in the recursive function. And in the event handler, I record the coordinate and time stamp and trigger the recursive function if it’s not started yet.

var x, y, eventTimestamp, isContinue;
$(document).on('mousemove', function(e){
  x = e.clientX, y = e.clientY, eventTimestamp = Date.now;
  if(!isContinue) draw();
});

The recursive function

This function does basically three things.

  • It adds and removes coordinates of the collection.
  • It checks and decides whether it continues and can be triggered by the event handler.
  • It draws all lines using adjacent coordinates and put it to the main canvas.

This function makes a delayed call to itself if mousemove event keeps firing. And when it executes it adds the new coordinate object to the collection. I can use this for the fading effect. So I put time stamps in coordinate objects and compare them with the newest one to decide the opacity value. By doing this, it draws lines with different fading effects and refreshes the canvas every time it executes. That's how the amination works.

function draw(){
  var currentTimestamp = Date.now, newPoint = {x: x, y: y, t: currentTimestamp};
  
  //check if to continue
  isContinue = currentTimestamp - eventTimestamp < 500;
  //add new coordinate
  if(isContinue){
 points.unshift(newPoint);
  }
  //remove faded out coordinates
  for(var i = 0; i < points.length;){
 currentTimestamp - points[i].t > 1000 ? points.splice(i, points.length) : i++;
  }
  //recursive call
  if(points.length){
 setTimeout(draw, 20);
  }
  
  var painter = $('<canvas>').attr('width', $(window).width()).attr('height', $(window).height())[0], pctx = painter.getContext('2d');
  //clear painter, ready to draw
  pctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  //draw all lines in the collection
  for (var i = 1; i < points.length; i++) {
 pctx.strokeStyle = "rgba(0,0,0," + Math.max(1-(currentTimestamp-points[i].t)/1000, 0) + ")";
 //umcomment this line to get dynamic line width
 //pctx.lineWidth = 25+75*Math.max(1-Math.sqrt(Math.pow(points[i].x-points[i-1].x,2)+Math.pow(points[i].y-points[i-1].y,2))/50,0);
 pctx.lineWidth = 50;
 pctx.beginPath();
 pctx.moveTo(points[i - 1].x, points[i - 1].y);
 pctx.lineTo(points[i].x, points[i].y);
 pctx.stroke();
  }
  var ctx = $('canvas')[0].getContext('2d'), img= $('img')[0];
  //copy-paste 
  ctx.drawImage(img, 0, 0, img.width, img.height);
  ctx.globalCompositeOperation = "destination-in";
  ctx.drawImage(painter, 0, 0);
  ctx.globalCompositeOperation = "source-over";
}

Handle resize

The background will automatically re-adjust to adopt the new width or height. So the image and canvases are also need to be taken care of.

$(window).on('resize', function(){
var img = $('img')[0];
  var windowHeight = $(window).height(), windowWidth = $(window).width(), windowRate = windowWidth/windowHeight;
  var imgHeight = img.naturalHeight, imgWidth = img.naturalWidth, imgRate = imgWidth/imgHeight;

  if(windowRate>=imgRate){
    $(img).attr('height', (imgHeight/imgWidth*windowWidth)).attr('width', windowWidth);
  }else{
    $(img).attr('width', (imgWidth/imgHeight*windowHeight)).attr('height', windowHeight);
  }
  $('canvas').attr('width', windowWidth).attr('height', windowHeight);
});

So that's the all of it. I put the demo effect on the background. Feel free to try.



UNBLUR BACKGROUND (PART 1)

UNBLUR BACKGROUND (PART 2)