Pages

Thursday, November 27, 2014

Rhino mocks and number of executions

Yesterday I found annoying flaw in Rhino mocks. I expected that when you said Repeat.Twice() the expectation is that the method would be executed exactly 2 times and if no - expectation should be failed. But looks like that it saying "at least twice". (Despite the fact that there is as well Repeat.AtLeastOnce())

So here is my setup:
public class Foo
{
 public virtual void DoStuff()
 {
  Console.WriteLine("DoStuff");
 }
}

public class Bar
{
 public Foo foo { get; set; }

 public void CallFoo(int max)
 {
  for (int i = 0; i < max; i++)
  {
   foo.DoStuff();
  }
 }
}

And that test is not failing unfortunately. You can even replace Times(2) with anything else - just the number of executions should be less then the actual number - test still would be green. And you can replace it with Once() or AtLeastOnce() - no difference at all!

[Test]
public void MockTest()
{
 var mock = MockRepository.GenerateMock();

 mock.Expect(x => x.DoStuff())
  .Repeat.Times(2);

 var bar = new Bar();
 bar.foo = mock;

 bar.CallFoo(3);

 mock.VerifyAllExpectations();
}

And here is how you can write that test to fail. Finally, checking for exact number of calls!

[Test]
public void MockTest2()
{
 var mock = MockRepository.GenerateMock();
 
 var bar = new Bar();
 bar.foo = mock;

 bar.CallFoo(3);

 mock.AssertWasCalled(x => x.DoStuff(),
       y => y.Repeat.Times(2));
}

Friday, November 21, 2014

jQuery FileUploader and WebForms

I`ve promised to write about my implementation of the async file uploader for the web forms application. The task was to implement it as a server control that can be added and reused on different pages (actually, in order to replace the old teleric control).

I decided to use this plugin. And the only problem was with a url to upload the file. As it should be available from the different pages I could not use the [WebMethod] so I`ve implemented a http handler to upload the file. Plus few additional changes to the client side - show the amount of files, add an option to delete uploaded file etc.

Here is my implementation:
 public class AsyncUploader : CompositeControl
 {
  private Panel _container;
  private Panel _uploadedFilesContainer;
  private HtmlInputFile _uploader;
    
  public string[] AllowedFileExtensions { get; set; }

  /// 
  /// Maximum file size in bytes
  /// 
  public int MaxFileSize { get; set; }

  /// 
  /// Gets or sets the virtual path of the folder, where RadUpload will automatically save the valid files after the upload completes.
  /// Note that existing files with the same name will be overwritten. As such it is best to append a unique identifier to the folder.
  /// 
  public string TargetPhysicalFolder { get; set; }

  public int MaxFilesCount { get; set; }
 
  protected string EscapedUniqueId { get { return Regex.Replace(UniqueID, "[$.\\s]", "_"); } }

  public List Files
  {
   get
   {
    EnsureChildControls();

    var targetFolder = TargetPhysicalFolder;

    var result = new List();

    foreach (var key in Page.Request.Form.AllKeys)
    {
     if (key.StartsWith(EscapedUniqueId + "savedFile_"))
     {
      var index = key.Replace(EscapedUniqueId + "savedFile_", "");
      var savedFileName = Page.Request.Form[key];
      result.Add(new UploadedFile
      {
       OriginalFileName = Page.Request.Form[EscapedUniqueId + "originalFile_" + index],
       SavedFileFullPath = Path.Combine(targetFolder, savedFileName),
       SavedFileName = savedFileName
      });
     }
    }
    return result;
   }
  }

  protected override void OnPreRender(EventArgs e)
  {
   if (_container.ClientIDMode != ClientIDMode.Static) throw new ArgumentException("ClientIDMode");

   var uniqueId = EscapedUniqueId;

   var options = new JavaScriptSerializer().Serialize(new
   {
    uniqueID = uniqueId,
    elementSelector = "#" + uniqueId,    
    sessionId = Page.Session.SessionID,
    maxFiles = MaxFilesCount,
    maxFileSize = MaxFileSize,
    extensions = AllowedFileExtensions != null && AllowedFileExtensions.Any()
     ? AllowedFileExtensions.ToDelimitedString("|")
     : "*",
    submitButtonsSelector = _submitButtonsToDisable
     .Select(x => "#" + x.ClientID)
     .ToDelimitedString(", ")
   });

   Page.Session[uniqueId + "_TargetFolder"] = TargetPhysicalFolder;

   var setUp = "".F(
    options);

   Page.ClientScript.RegisterClientScriptBlock(GetType(), "setup" + uniqueId, setUp);

   base.OnPreRender(e);
  }

  protected override void CreateChildControls()
  {
   _container = new Panel();
   _container.ClientIDMode = ClientIDMode.Static;
   _container.ID = EscapedUniqueId;

   _uploader = new HtmlInputFile();
   _uploader.Attributes.Add("class", "filePicker");
   if (MaxFilesCount > 1)
    _uploader.Attributes.Add("multiple", "true");

   _uploadedFilesContainer = new Panel();
   _uploadedFilesContainer.Attributes.Add("class", "uploadedFiles");
   _uploadedFilesContainer.ID = RandomString.Generate(20);

   var errorsContainer = new Panel();
   errorsContainer.Attributes.Add("class", "uploadErrors");
      
   _container.Controls.Add(_uploader);
   _container.Controls.Add(_uploadedFilesContainer);
   _container.Controls.Add(errorsContainer);

   this.Controls.Add(_container);

   base.CreateChildControls();
  }

  protected override void OnLoad(EventArgs e)
  {
   //Register resources (here I`ve used Peter Blum)
   ClientScriptLibrary
    .RegisterEmbeddedResource(
    typeof(AsyncUploader),
    "jquery.ui.widget.js",
    ClientDependencyType.Javascript);

   ClientScriptLibrary
    .RegisterEmbeddedResource(
    typeof(AsyncUploader),
    "jquery.fileupload.js",
    ClientDependencyType.Javascript);

   ClientScriptLibrary
    .RegisterEmbeddedResource(
    typeof(AsyncUploader),
    "AsyncUploadFormItem.js",
    ClientDependencyType.Javascript);

   ClientScriptLibrary
    .RegisterEmbeddedResource(
    typeof(AsyncUploader),
    "AsyncUploadFormItem.css",
    ClientDependencyType.CSS);

   base.OnLoad(e);
  }
 } 

 public class UploadedFile
 {
  public string OriginalFileName { get; set; }
  public string SavedFileName { get; set; }
  public string SavedFileFullPath { get; set; }
 }

var setupfileUpload = function (options) {

    var $filePicker = $(options.elementSelector + ' .filePicker');
    var $uploadedFiles = $(options.elementSelector + ' .uploadedFiles');
    var $uploadErrors = $(options.elementSelector + ' .uploadErrors'); 

    $filePicker.fileupload({
        url: 'fileUpload.axd',
        dropZone: $filePicker,
        add: function (e, data) {
            cleanExceptionsPanel();

            var count = $uploadedFiles.find('.sentFile').length;
            
            //client validation by file size and file type
            if (!isUploadLimit(options.maxFiles, count) ||
                !isFileValid(data.files[0].size, data.files[0].name)) return;

            //to support duplicated files the div id should be unique for different files - and that Id should be passed to the handler.
            var divId = 'id' + (new Date()).getTime();

            //submit the form with 2 additional parameters - where to save and file id
            data.formData = { targetFolder: options.targetFolder, fileId: divId, sessionId: options.sessionId, controlid: options.uniqueID };
            var jqXHR = data.submit();
            
            //append a div containing inputs for a given file
            var div = $('
'); div.append('
'); div.append('' + data.files[0].name + ''); var cancelButton = $('x'); cancelButton.on('click', function () { //delete uploaded file and/or cancel the upload process cleanExceptionsPanel(); jqXHR.abort(); var savedFile = $(this).parent().find('.savedFile').val(); if (savedFile) $.post('fileUpload.axd', { fileName: savedFile, deleteRequest: true, sessionId: options.sessionId, controlid: options.uniqueID }); $(this).parent().remove(); }); div.append(cancelButton); div.append(''); $uploadedFiles.append(div); }, done: function (e, data) { var res = jQuery.parseJSON(data.result); var div = $uploadedFiles.find('#' + res.FileId); //remove the progress bar and insert a 'complete' dot instead div.find('.progressbar').remove(); div.prepend('
'); var count = div.data('filenumber'); div.append(''); //manually hide the validation error var validationError = $(options.elementSelector + ' span.errors .errorMessage'); validationError.css('visibility', 'hidden'); validationError.css('display', 'none'); }, progress: function (e, data) { var p = parseInt(data.loaded / data.total * 100, 10); if (typeof p === 'number') { var div = $uploadedFiles.find('#' + data.formData.fileId); var progress = div.find('.progress'); progress.css('width', p + '%'); } }, fail: function (e, data) { var div = $uploadedFiles.find('#' + data.formData.fileId); //remove the progress bar and insert a 'fail' dot instead div.find('.progressbar').remove(); div.prepend('
'); } }); var cleanExceptionsPanel = function() { $uploadErrors.html(''); } var isUploadLimit = function(maxCount, currentCount) { if (maxCount <= 0) return true; if (maxCount == 1) { // replace existing file $uploadedFiles.find('.deleteUploadedFile').each(function() { $(this).click(); }); } else if (maxCount <= currentCount) { $uploadErrors.append('Maximum number of files is attached'); return false; } return true; }; var isFileValid = function (filesize, filename) { if (filesize > options.maxFileSize) { $uploadErrors.append('File is too large to be uploaded'); return false; } var pattern = '.+?\.(' + options.extensions + ')'; if (!filename.match(new RegExp(pattern, 'i'))) { $uploadErrors.append('File type is not supported and cannot be uploaded'); return false; } return true; }; var escapeFileName = function (fileName) { return fileName.replace(/ |\.|#/g, '_'); } if (options.submitButtonsSelector) { $(options.submitButtonsSelector) .prop('disabled', false) .each(function() { { $(this).attr('title', $(this).attr('data-oldtitle')); } }); } }; }

 public class FileUploadHandler : IHttpHandler, IReadOnlySessionState
 {
  private static readonly Logger _log = LogManager.GetCurrentClassLogger();

  private void DeleteFile(HttpContext context)
  {
   // Make sure to sanitise the filename by calling Path.GetFileName. This will
   // prevent deletions from folders other than the target folder (which is known only
   // by the server)
   var filename = Path.GetFileName(context.Request.Form["filename"]);
   var controlId = context.Request.Form["controlId"];
   var path = context.Session[controlId + "_TargetFolder"].ToString();
   
   var targetFilename = Path.Combine(path, filename);

   if (File.Exists(targetFilename))
    File.Delete(targetFilename);
  }

  private void UploadFile(HttpContext context)
  {
   Parse(context.Request.InputStream, Encoding.UTF8);

   var controlId = context.Request.Form["controlId"];
   var fileId = context.Request.Form["fileId"];
   
   var path = context.Session[controlId + "_TargetFolder"].ToString();
   var targetFolder = Directory.CreateDirectory(path).FullName;
   var targetFilename = Path.Combine(targetFolder, _filename);

   // Handle existing files by incrementing counter
   int counter = 1;
   while (File.Exists(targetFilename))
   {
    counter++;
    targetFilename = Path.Combine(targetFolder,
     Path.GetFileNameWithoutExtension(_filename) + counter + Path.GetExtension(_filename));
   }

   using (var file = File.Create(targetFilename))
   {
    file.Write(_fileContents, 0, _fileContents.Length);
   }

   context.Response.Write(new JavaScriptSerializer()
    .Serialize(new
    {
     OriginalFile = _filename, 
     SavedFile =  Path.GetFileName(targetFilename),
     FileId = fileId
    }));
  }

  public void ProcessRequest(HttpContext context)
  {
   var sessionId = context.Request.Form["sessionId"];

   if (context.Session == null || context.Session.SessionID != sessionId)
    throw new InvalidOperationException("Wrong session state during the file upload operation");

   if (context.Request.Form["deleteRequest"] != null)
   {
    DeleteFile(context);
   }
   else
   {
    UploadFile(context);
   }
  }

  private byte[] ToByteArray(Stream stream)
  {
   byte[] buffer = new byte[32768];
   using (MemoryStream ms = new MemoryStream())
   {
    while (true)
    {
     int read = stream.Read(buffer, 0, buffer.Length);
     if (read <= 0)
      return ms.ToArray();
     ms.Write(buffer, 0, read);
    }
   }
  }

  private int IndexOf(byte[] searchWithin, byte[] serachFor, int startIndex)
  {
   int index = 0;
   int startPos = Array.IndexOf(searchWithin, serachFor[0], startIndex);

   if (startPos != -1)
   {
    while ((startPos + index) < searchWithin.Length)
    {
     if (searchWithin[startPos + index] == serachFor[index])
     {
      index++;
      if (index == serachFor.Length)
      {
       return startPos;
      }
     }
     else
     {
      startPos = Array.IndexOf(searchWithin, serachFor[0], startPos + index);
      if (startPos == -1)
      {
       return -1;
      }
      index = 0;
     }
    }
   }

   return -1;
  }

  private void Parse(Stream stream, Encoding encoding)
  {
   // Read the stream into a byte array
   byte[] data = ToByteArray(stream);

   // Copy to a string for header parsing
   string content = encoding.GetString(data);

   // The first line should contain the delimiter
   int delimiterEndIndex = content.IndexOf("\r\n");

   if (delimiterEndIndex > -1)
   {
    string delimiter = content.Substring(0, content.IndexOf("\r\n"));

    // Look for Content-Type
    Regex re = new Regex(@"(?<=Content\-Type:)(.*?)(?=\r\n\r\n)");
    Match contentTypeMatch = re.Match(content);

    // Look for filename
    re = new Regex(@"(?<=filename\=\"")(.*?)(?=\"")");
    Match filenameMatch = re.Match(content);

    // Did we find the required values?
    if (contentTypeMatch.Success && filenameMatch.Success)
    {
     // Set properties
     this._contentType = contentTypeMatch.Value.Trim();
     this._filename = filenameMatch.Value.Trim();

     // Get the start & end indexes of the file contents
     int startIndex = contentTypeMatch.Index + contentTypeMatch.Length + "\r\n\r\n".Length;

     byte[] delimiterBytes = encoding.GetBytes("\r\n" + delimiter);
     int endIndex = IndexOf(data, delimiterBytes, startIndex);

     int contentLength = endIndex - startIndex;

     // Extract the file contents from the byte array
     byte[] fileData = new byte[contentLength];

     Buffer.BlockCopy(data, startIndex, fileData, 0, contentLength);

     this._fileContents = fileData;
    }
   }
  }

  private string _contentType;
  public string _filename;
  public byte[] _fileContents;


  public bool IsReusable { get { return false; } }
 }

Here I`ve used the Session to store the target folder for files as this is safer. Hope this will help someone :)

Wednesday, November 19, 2014

Be careful with Path.Combine

Yesterday I was pointed out that there is a security problem with my code. I was coding the async file uploader (will post it little later) and it is able to delete unused files as well. It is done with a post of a filename and that filename is stored in a hidden input. Everything is fine with that but the problem is in my usage of the Path.Combine method.

Here is a snippet:
 var p1 = "C:\\Test";
 var p2 = "C:\\NOT_A_TEST\\File.txt";
 var p3 = "File.txt";
 
 Console.WriteLine(Path.Combine(p1, p2)); //result is "C:\NOT_A_TEST\File.txt"
 Console.WriteLine(Path.Combine(p1, p3)); //all good - C:\Test\File.txt
 
 //and 2 safe methods:
 Console.WriteLine(Path.Combine(p1, Path.GetFileName(p2))); 
 Console.WriteLine(Path.Combine(p1, Path.GetFileName(p3)));

So if you want to use path combine - make sure that the last part of it is only a filename, not the whole path as it can overwrite the whole result! And as aa side-note
 Console.WriteLine(Path.GetFileNameWithoutExtension("C:\\Test\\test.txt")); 

Will return 'test', so it is as safe as Path.GetFileName.

Wednesday, October 22, 2014

The Simplest WPF Diagram Designer - Part 3

2 weeks ago I started a series of tips about WPF Diagram Designer (part1, part2). But as you know, there is nothing new under the sun. I have been pointed that there were few similar articles already posted on CodeProject so I`ve decided to post my next part of work here. I will add basic keyboard binding and RavenDB/XML save-load functionality. The source code can be found here: https://bitbucket.org/JleruOHeP/mywpfdesigner/src.

Keyboard commands
I have added 2 commands: Delete the current element and edit the text of the current element.
For the sake of simplicity I have changed the meaning of the Tag property for shapes. Now it stores the shape entity itself instead of just the Id. Because of that it became much faster to work with them. Because user can`t select the line we need to delete the connector, line and the opposite connector when someone selects the connector and presses Del. Now the Connector should know not only its host but as well its line. Ok. And the ChangeText command is really simple - as we already have all needed functionality.

RavenDB
I wanted to pick some embedded NoSQL database and the RavenDB was an obvious choice. I added wrapper class for that matter with a static EmbeddableDocumentStore DocumentStore and 2 methods - to Save the whole model or to load all objects of a specific type. With that implementation there is nothing complex about saving and for loading you have to clear the Canvas, reinitialize lines (because they do not store their coordinates - only connectors; connectors should be reconnected to their hosts), redraw the shapes, and finally recreate the Model class (assign proper commands and set it as a DataContext for a Window).

Also as the connectors are not used as a separate entity - I removed them from the model class. Now there are only 2 collections - for lines and shapes.

XML Serialization
It is really simple. Our Model class already has everything it needs - so we can easily use the XmlSerializer class.

 var serializer = new XmlSerializer(typeof(MyCanvasModel));
 using (var writer = new StreamWriter("data.xml"))
 {
  serializer.Serialize(writer, Model);
  writer.Close();
 }

The only thing we need to add - is [XmlIgnore] attribute for all the commands because they cant be serialized.

Sunday, September 7, 2014

WPF Save command

What can be easier then type

In WPF it is a designed way to interact with user. It is much simpler then binding to events as it gives you a better flexibility with CanExecute property and you code becomes reusable.

I wanted to implement that command and checked google. I was surprised to find that there were not any direct explanation in first 2-3 results. Even more, MSDN has a really bad explanation of what that command is and as an example there is a source code for Paste command. I am not sure why :)

So just in case - if you want to bind Save command you can CommandBinding to your own handlers like this:

XAML

    
        
        
    
    
        
        
    


C#
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = true; //your logic
        }

        private void SaveCommand_OnExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            MessageBox.Show("Save!");
        }

        private void OpenCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = true;
        }

        private void OpenCommand_OnExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            MessageBox.Show("Open");
        }
    }

Sunday, August 10, 2014

Visual studio shortcuts

Visual Studio as any other IDE has many keyboard shortcuts and other useful tricks. And, naturally, users can adjust them. However there are developers who are not using most of them at all and even more of developers use only small subset of them. Partly because those hidden shortcuts are not that useful in general and can be quite specific and partly because of their hidden nature.

Let me list some of those I found:
First of all - navigation ones.
CTRL + arrow left or arrow right: Cursor position would be moved one word (not character!) left or right;
CTRL + arrow up or arrow down: The whole screen would be moved by cursor position would be still the same.
CTRL + [ or ]: move to the corresponding opening or closing bracket. Especially useful if you have a long cycle or if statement.
CTRL + Tab: navigation through opened tabs. If you have 2 opened files - you can switch them.

Next - some formatting tricks:
CTRL + SHIFT + U: make the selection upper case. One of the applications I have used: database store list of string values. And those can be upper case or lower case, and you need to compare them against a list of predefined constants. If you were given that list of constants in lower case you can transform them quite easily!
CTRL + L: removes the whole line

Selection:
If you hold SHIFT during cursor movement - you can select that area. More interesting options - you can hold SHIFT with other combinations. Like CTRL + SHIFT + Arrow Left would select the whole previous word; CTRL + SHIFT + END would select the whole text from the current cursor position to the very end of the file;
If you hold ALT you can select rectangle in the text. And it doesn't needs to be in the beginning of the line! Try it - hold ALT + left mouse button + move the button! Even more interesting - you can replace the selection with typing or pasting and it will replace each of selected lines!

Build:
ALT + B, U: builds current project only. It is like a separate keystrokes: Alt to activate menu, B to access Build menu and U to build current project. It can be shortened but that shortcut will always work.

Hope this helped!


Wednesday, August 6, 2014

Developing: for money or for fun?

I was always interested in development - from the very childhood when I was able to build simplest programs using Basic and Commodore 64 through school years and Pascal into current time and C#. For me the development process is fun.
But it is not the only thing I care about development. Much more important side of development for me is my salary. Simply because without that I would not be able to eat :) Though, it messes a little with a fun factor. I have to solve problems that might not be that interesting and I have to spend time on those tasks. Moreover I have to spend my energy and quite often my brains just don`t want to do anymore thinking after work.
Well, mainly because of the last statement I am asking myself - what is more important for me? If it is money than I can easily find another job with a better payments and/or better working hours. And if it is programming itself then why not to start my own personal business?
I am sure that every developer had asked himself similar questions... What were the answers? As I stated in the beginning - I think that money is not that important for me. Therefore I should start something :) So I've decided to make a simple idea planer. I imagine it would be WPF application with an intuitive GUI and a very simple functionality. Why not? It can be fun and can evolve into something bigger!