Pages

Thursday, December 4, 2014

YOW 2014

Thanks to my company, stratton, I was able to attend the YOW! conference in Melbourne. It was a 2-day software conference and it was amazing :). I really liked everything, especially as it was my first big conference.
For now I want to leave some notes about sessions I visited.

Day 1:

1) User story mapping: Discover the whole story by Jeff Patton. It was good especially as Jeff is a great presenter. But the content was a little bit childish - too easy and nothing special.
2) Make impacts, not software by Gojko Adzic. Second session was not very deep as well and I suppose nobody would expect to get some out-of-the box solutions; so from that session I was able to get few ideas at least like how to piss of scrum masters :) Or why we don't actually need all that test coverage.
3) Cool things about D - why and how we use it at Facebook by Andrei Alexandrescu. Finally that was a great speech. Andrei not only covered the simple usage of D (in a great way! so I started to plan when I will download its compiler) but as well talked about purity and some general programming concepts. It was useful and interesting. I would say that it was one of the best sessions from today.
4) Groovy: the awesome parts by Paul King. I wanted to get into groovy a little and the session started with an idea that groovy is the same thing for java as D for C++ but, unfortunately, this topic was not of the same quality as previous one. Frankly, it was just boring... Basic examples and no real value. At least for me.
5) Programming in the large: Architecture and experimentation by Mark Hibberd. Mark did a great job of general description of the general programming myths with a real examples of what his team doing. He even admitted that they could do it better! It was fun to listen to him and it was really motivational. Once again - it made me think of what and how I would do in the future. Thank you Mark.
6) Functionally obvious and succinct by Edward Kmett. This session was hard. Really hard. First of all - I had never seen haskel code before and secondly - it was all about optimization so a lot of "o(log(n))". And during the session I almost constantly had a question "wat" in my head. But later on the way home I rethought what I heard and it comes that this topic is interesting for me and I would like to try to implement the same data structure with C# (and will post about it soon).

Day 2:

1) Reactive, message driven and scalable by Todd L. Montgomery. Todd was talking about the past, present and future of the http protocol. Interesting topic but it was pretty much summarized by his own words that it is likely that most deevlopers wont notice any difference - it is too low level.
2) The scaling dilemma by Mary Poppendieck. Another session about the "agile way". This time on how to scale the agile approach for the enterprise level. Frankly, just another agile talk - nothing new. Maybe those agile coaches just aiming for a real general things?
3) How we went from 1 million to 1 billion events without throwing everything away by Julian Giuca. That was just boring - Julian was speaking about their approach to frameworks, made a lot of examples and he is a good speaker. But the topic was just bad, nothing specific and nothing useful - just a bunch of general ideas that everyone can produce.
4) How to undo almost anything with Git by Peter Bell. Finally, that was the only session on that conference (for me) where the speaker was not only showing slides but actually type something. During that speech I was enjoying the real console and actual usage of the Git. That was really fun!
5) Agility at the essence of software architecture by Simon Brown. Luckily, that session was not a common agile talk. From that one I was able to catch some new ideas and approaches, I am very pleased that I have seen it. Sketching is great :)
6) Pippi's book of the dead trading cards by Elizabethe Kramer. Well, the last session of the conference was the worst one. It was more about psychology then development and it was really bad. Elizabeth told us how she was able to settle one unpleasant situation  for one company. But why I would like to know it - I don't know :)

So that was all. Unfortunately, I was not able to network at all, my social skills need a lot of improvements! 

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!