Archive

Posts Tagged ‘.NET’

HowTo: Display version information on WinForm title

This is my contribution to
http://stackoverflow.com/questions/7178725/version-number-in-winform-form-text

I’m using the following at the WinForm of the WebCapture tool I’m making for ClipFlair:

public MainForm()
{
  InitializeComponent();
  Version version = Assembly.GetExecutingAssembly().GetName().Version;
  Text = Text + " " + version.Major + "." + version.Minor + 
" (build " + version.Build + ")"; //change form title }

Not showing revision number to the user, build number is enough technical info

Make sure your AssemblyInfo.cs ends in the following (remove the version it has there by default) for VisualStudio to autoincrement build and revision number. You have to update major and minor versions yourself at every release (update major version for new features, minor version when you do just fixes):

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version 
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build
// and Revision Number by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.*")]

Update: Since now WebCapture is ClickOnce-deployed from the web (also has nice autoupdate support thanks to ClickOnce) and I want to avoid user confusion by showing the published version of the app (as the ClickOnce installation page does) on the WinForm titlebar, I now use the following code instead, based on a relevant post I found on the web:

public MainForm()

{

  InitializeComponent();

  ShowVersion();

}

private void ShowVersion()

{

  Version version = (ApplicationDeployment.IsNetworkDeployed)?

    ApplicationDeployment.CurrentDeployment.CurrentVersion :

    Assembly.GetExecutingAssembly().GetName().Version;

    //if network deployed show published version (as the web install page)

    
  Text = Text + " " + version.Major + "." + version.Minor + "."

                     + version.Build + "." + version.Revision;

                       //change form title

}

BTW, speaking of ClickOnce, I noticed that if at the autoupdate dialog shown when you launch the app from its desktop shortcut (and there’s a new version available) you press “Skip”, it won’t offer you this update anymore when you launch an app (I hope that when an even newer update is there it will prompt you), but you can override that behaviour by going to the installation page for your ClickOnce app (where you installed it from in the first place) and press the “Install” button there.

HowTo: Save screenshot of a control hosted on a WinForm

I’m trying to automate grabbing of activity screenshots from ClipFlair Activity Gallery, but most command-line screen capturing tools, like SiteShoter and IECapt fail to get an image from Silverlight content (they get just a background color).

Some other opensource tool that I could tweek was ThumbPage, but  that didn’t support command-line and needed to disable Silverlight hardware acceleration in the webpage source (that is not define enableGPUAcceleration Silverlight param or set it to false) to grab the Silverlight content too in the image. This is probably because that tool was written in VB6 and must be using plain GDI instead of GDI+ or other newer graphics API, but I’m not sure if that’s the issue with the h/w acceleration.

So, I started making my own WinForms-based tool in Visual Studio. The following sample code is based on ArsenMkrt’s reply at StackOverflow, but this one allows you to capture a control in your form (my tool has a WebBrowser control in it and want to capture just its display). Note the use of PointToScreen method:

//Project: WebCapture
//Filename: ScreenshotUtils.cs
//Author: George Birbilis (http://zoomicon.com)
//Version: 20130820

using System.Drawing;
using System.Windows.Forms;

namespace WebCapture
{
  public static class ScreenshotUtils
  {

    public static Rectangle Offseted(this Rectangle r, Point p)
    {
      r.Offset(p);
      return r;
    }

    public static Bitmap GetScreenshot(this Control c)
    {
      return GetScreenshot(new Rectangle(c.PointToScreen(Point.Empty), c.Size));
    }

    public static Bitmap GetScreenshot(Rectangle bounds)
    {
      Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height);
      using (Graphics g = Graphics.FromImage(bitmap))
        g.CopyFromScreen(new Point(bounds.Left, bounds.Top), 
                         Point.Empty, bounds.Size);
      return bitmap;
    }

    public const string DEFAULT_IMAGESAVEFILEDIALOG_TITLE = "Save image";
    public const string DEFAULT_IMAGESAVEFILEDIALOG_FILTER = 
"PNG Image (*.png)|*.png|JPEG Image (*.jpg)|*.jpg|Bitmap Image (*.bmp)|
*.bmp|GIF Image (*.gif)|*.gif";

//*** NOTE: the string above is a single line, split it for posting here ***//

    public static SaveFileDialog GetImageSaveFileDialog(
      string title = DEFAULT_IMAGESAVEFILEDIALOG_TITLE, 
      string filter = DEFAULT_IMAGESAVEFILEDIALOG_FILTER)
    {
      SaveFileDialog dialog = new SaveFileDialog();

      dialog.Title = title;
      dialog.Filter = filter;

      return dialog;
    }

    public static void ShowSaveFileDialog(this Image image, 
                                          IWin32Window owner = null)
    {
      using (SaveFileDialog dlg = GetImageSaveFileDialog())
        if (dlg.ShowDialog(owner) == DialogResult.OK)
          image.Save(dlg.FileName);
    }

  }
}

Note the “using” clause above, it makes sure that the local variable g of type Graphics is Disposed immediately after the respective clause exits, instead of leaving any allocated resources for the garbage collector to handle. Plus the compiler makes sure the “g” variable isn’t use at the following code in that method by mistake (which could occur if you disposed it there by hand instead).

Having the Bitmap object you can just call Save on it:

private void btnCapture_Click(object sender, EventArgs e)
{
  webBrowser.GetScreenshot().Save("C://test.jpg", ImageFormat.Jpeg);
}

The above assumes the Garbage Collector (aka GC) will grab the bitmap, but maybe it’s better to assign the result of someControl.getScreenshot() to a Bitmap variable, then dispose that variable manually when finished, especially if you’re doing this grabbing often (say you have a list of webpages you want to load and save screenshots of them):

private void btnCapture_Click(object sender, EventArgs e)
{
  Bitmap bitmap = webBrowser.GetScreenshot();
  bitmap.ShowSaveFileDialog();
  bitmap.Dispose(); //release bitmap resources
}

 

Update:

Now WebCapture tool is ClickOnce-deployed from the web (also has nice autoupdate support thanks to ClickOnce) and you can find its source code at http://ClipFlair.codeplex.com.

Speaking of ClickOnce, I noticed that if at the autoupdate dialog shown when you launch the app from its desktop shortcut (and there’s a new version available) you press “Skip”, it won’t offer you this update anymore when you launch an app (I hope that when an even newer update is there it will prompt you), but you can override that behaviour by going to the installation page for your ClickOnce app (where you installed it from in the first place) and press the “Install” button there.

HowTo: Bind ASP.net control to list of files or folders

At ClipFlair Gallery metadata input pages for Activities and Clips I had to bind an ASP.net control to a list of files and folders respectively and although I found a Folder Contents DataSource control, it didn’t cover my needs (like filtering of a folder contents).

I just contributed my solution using .NET Anonymous Types and LINQ (assumes a using System.Linq clause) to:

http://stackoverflow.com/questions/1331793/bind-repeater-to-a-list-of-files-and-or-folders

private string path = HttpContext.Current.Server.MapPath("~/activity");

   protected void Page_Load(object sender, EventArgs e)
   {      
if (!IsPostBack) { //only at page 1st load
listItems.DataSource = Directory.EnumerateFiles(path, "*.clipflair") .Select(f => new { Filename=Path.GetFileName(f) });
listItems.DataBind(); //must call this } }

The above snippet gets all *.clipflair files from ~/activity folder of a web project

Update: using EnumerateFiles (availabe since .NET 4.0) instead of GetFiles since this is more efficient with LINQ queries. GetFiles would return a whole array of filenames in memory before LINQ had a chance to filter it.

The following snippet shows how to use multiple filters, which GetFiles/EnumerateFiles don’t support themselves:

private string path = HttpContext.Current.Server.MapPath("~/image");
private string filter = "*.png|*.jpg";

protected void Page_Load(object sender, EventArgs e)
{
  _listItems = listItems; 
  
  if (!IsPostBack)
  {
    listItems.DataSource =
      filter.Split('|').SelectMany(
oneFilter => Directory.EnumerateFiles(path, oneFilter)
.Select(f => new { Filename = Path.GetFileName(f) })
); listItems.DataBind(); //must call this if (Request.QueryString["item"] != null) listItems.SelectedValue = Request.QueryString["item"];
//must do after listItems.DataBind } }


The snippet below shows how to get all directories from /~video folder and also filters them to select only directories that contain a .ism file (Smooth Streaming content) with the same name as the directory (e.g. someVideo/someVideo.ism)

using System;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.UI.WebControls;

namespace ClipFlair.Gallery
{
 public partial class VideoMetadataPage : System.Web.UI.Page
 {

  private string path = HttpContext.Current.Server.MapPath("~/video");

  protected void Page_Load(object sender, EventArgs e)
  {
    if (!IsPostBack) { //only at page 1st load
listItems.DataSource = Directory.GetDirectories(path) .Where(f =>(Directory.EnumerateFiles(f,
Path.GetFileName(f)+".ism").Count()!=0)) .Select(f => new { Foldername = Path.GetFileName(f) }); //when having a full path to a directory don't use Path.GetDirectoryName //(gives parent directory), //use Path.GetFileName instead to extract the name of the directory listItems.DataBind(); //must call this } }

 

The examples above are from a DropDownList, but it’s the same logic with any ASP.net control that supports Data Binding (note I’m calling Foldername the data field at the 2nd snippet and Filename at the 1st one, but could use any name, need to set that in the markup):

        <asp:DropDownList ID="listItems" runat="server" AutoPostBack="True" 
          DataTextField="Foldername" DataValueField="Foldername" 
          OnSelectedIndexChanged="listItems_SelectedIndexChanged"
          />

HowTo: Remove invalid filename characters in .NET

In ClipFlair Studio I use DotNetZip (Ionic.Zip) library for storing components (like the activity and its nested child components) to ZIP archives (.clipflair or .clipflair.zip files). Inside the ZIP archive its child components have their own .clipflair.zip file and so on (so that you could even nest activities at any depth) which construct their filename based on the component’s Title and ID (a GUID)

However, when the component Title used characters like " (double-quote) which are not allowed in filenames, then although Ionic.Zip created the archive with the double-quotes in the nested .clipflair.zip filenames, when trying to load those ZipEntries into a memory stream it failed. Obviously I had to filter those invalid filename characters (I opted to remove them to make those ZipEntry filenames a bit more readable/smaller).

So I added one more extension method for string type at StringExtensions static class (Utils.Silverlight project), based on info gathered from the links from related stackoverflow question. To calculated version of a string s without invalid file name characters, one can do s.ReplaceInvalidFileNameChars() or optionally pass a replacement token parameter (a string) to insert at the position of each char removed.

public static string ReplaceInvalidFileNameChars(this string s,
string replacement = "") { return Regex.Replace(s, "[" + Regex.Escape(new String(System.IO.Path.GetInvalidPathChars())) + "]", replacement, //can even use a replacement string of any length RegexOptions.IgnoreCase); //not using System.IO.Path.InvalidPathChars (deprecated insecure API) }

For more info on Regular Expressions see http://www.regular-expressions.info/ and http://msdn.microsoft.com/en-us/library/hs600312.aspx


BTW, note that to convert the char[] returned by System.IO.Path.GetInvalidPathChars() to string we use new String(System.IO.Path.GetInvalidPathChars()).

It’s unfortunate that one can’t use ToString() method of char[] (using Visual Studio to go to definition of char[].ToString() takes us to Object.ToString() which means the array types don’t overload the virtual ToString() method of Object class to return something useful).


Another thing to note is that we don’t use System.IO.Path.InvalidPathChars field which is deprecated for security reasons, but use System.IO.Path.GetInvalidPathChars() method instead. MSDN explains the security issue, so better avoid that insecure API to be safe:

Do not use InvalidPathChars if you think your code might execute in the same application domain as untrusted code. InvalidPathChars is an array, so its elements can be overwritten. If untrusted code overwrites elements of InvalidPathChars, it might cause your code to malfunction in ways that could be exploited.

.NET String extension methods to check for array of prefixes or suffixes

Seems StartsWith and EndsWith methods of String class in .NET are missing a version that accepts multiple (as an array) prefixes or suffixes respectively when testing the string. To achieve this I just added the following extension methods to StringExtensions class (of Utils.Extensions namespace) under Utils.Silverlight project at the ClipFlair source code.

public static bool StartsWith(
this string s,
string[] suffixes,
StringComparison comparisonType = StringComparison.CurrentCulture) { foreach (string suffix in suffixes) if (s.StartsWith(suffix, comparisonType)) return true; return false; } public static bool EndsWith(
this string s,
string[] suffixes,
StringComparison comparisonType = StringComparison.CurrentCulture) { foreach (string suffix in suffixes) if (s.EndsWith(suffix, comparisonType)) return true; return false; }

 

To use them, you add a reference to Utils.Silverlight project to your own one and then add a using clause for the namespace that hosts a static class with these extension methods (e.g. “using Utils.Extensions;”) and then you can use them on any String at the respective source file. Can even use them on literal strings, since most .NET compilers support Boxing of literals into respective types.

I’m using a default value for the comparisonType method argument to make it optional. I use StringComparison.CurrentCulture as the default value for it (performing a word case-sensitive and culture-sensitive comparison using the current culture), as Microsoft is doing at “String.StartsWith(String)” method. However, do note the following text from that method’s documentation:

Notes to Callers

As explained in Best Practices for Using Strings in the .NET Framework, we recommend that you avoid calling string comparison methods that substitute default values and instead call methods that require parameters to be explicitly specified. To determine whether a string begins with a particular substring by using the string comparison rules of the current culture, call the StartsWith(String, StringComparison) method overload with a value of StringComparison.CurrentCulture for its comparisonType parameter.

.NET String class extensions to replace prefix or suffix

Just added the following extension methods to StringExtensions class (of Utils.Extensions namespace) under Utils.Silverlight project at the ClipFlair source code.

public static string ReplacePrefix(
this string s,
string fromPrefix,
string toPrefix,
StringComparison comparisonType = StringComparison.CurrentCulture) { return (s.StartsWith(fromPrefix, comparisonType)) ?
toPrefix + s.Substring(fromPrefix.Length) : s; } public static string ReplacePrefix(
this string s,
string[] fromPrefix,
string toPrefix,
StringComparison comparisonType = StringComparison.CurrentCulture) { foreach (string prefix in fromPrefix) if (s.StartsWith(prefix, comparisonType)) return toPrefix + s.Substring(prefix.Length); return s; } public static string ReplaceSuffix(
this string s,
string fromSuffix,
string toSuffix,
StringComparison comparisonType = StringComparison.CurrentCulture) { return (s.EndsWith(fromSuffix, comparisonType)) ?
s.Substring(0, s.Length - fromSuffix.Length) + toSuffix : s; } public static string ReplaceSuffix(
this string s,
string[] fromSuffix,
string toSuffix,
StringComparison comparisonType = StringComparison.CurrentCulture) { foreach (string suffix in fromSuffix) if (s.EndsWith(suffix, comparisonType)) return s.Substring(0, s.Length - suffix.Length) + toSuffix; return s; }

 

To use them, you add a reference to Utils.Silverlight project to your own one and then add a using clause for the namespace that hosts a static class with these extension methods (e.g. “using Utils.Extensions;”) and then you can use them on any String at the respective source file. Can even use them on literal strings, since most .NET compilers support Boxing of literals into respective types.

e.g.

s = s.ReplacePrefix("https://&quot;, "http://&quot;, StringComparison.OrdinalIgnoreCase);

//– converts https:// prefix to http:// ignoring character case

or

s = s.ReplacePrefix(new String[]{"https://&quot;, "http://&quot;}, "", StringComparison.OrdinalIgnoreCase);

//– removes https:// or http:// prefix ignoring character case

 

Update:

I added a default value for the comparisonType method argument to make it optional. I use StringComparison.CurrentCulture as the default value for it (performing a word case-sensitive and culture-sensitive comparison using the current culture), as Microsoft is doing at “String.StartsWith(String)” method. However, do note the following text from that method’s documentation:

Notes to Callers

As explained in Best Practices for Using Strings in the .NET Framework, we recommend that you avoid calling string comparison methods that substitute default values and instead call methods that require parameters to be explicitly specified. To determine whether a string begins with a particular substring by using the string comparison rules of the current culture, call the StartsWith(String, StringComparison) method overload with a value of StringComparison.CurrentCulture for its comparisonType parameter.

HowTo: format XML output of DataContractSerializer

based on the other samples posted at StackOverflow on how to format XML created by DataContractSerializer, that use XmlWriter, here’s a version (from ClipFlair source code) that works with streams (and Ionic.Zip library in specific).

It also shows how the code is when you don’t apply formatting (using conditional compilation).  Just comment out the #define (prefix it with //) to make it write unformatted XML.

 

#define WRITE_FORMATTED_XML

using System.Xml;

namespace ClipFlair.Windows
{

  public partial class BaseWindow : FloatingWindow
  {

    //...

    #if WRITE_FORMATTED_XML
    private static XmlWriterSettings XML_WRITER_SETTINGS = 
new XmlWriterSettings() { Indent=true, IndentChars=" "}; #endif //... public virtual void SaveOptions(ZipFile zip, string zipFolder = "")
//THIS IS THE CORE SAVING LOGIC { if (SavingOptions != null) SavingOptions(this, null); //notify any listeners View.Busy = true; try { ZipEntry optionsXML =
zip.AddEntry(zipFolder + "/" + View.GetType().FullName + ".options.xml", new WriteDelegate((entryName, stream) => { DataContractSerializer serializer =
new DataContractSerializer(View.GetType());
//assuming current View isn't null #if WRITE_FORMATTED_XML using (XmlWriter writer = XmlWriter.Create(stream, XML_WRITER_SETTINGS)) serializer.WriteObject(writer, View); #else serializer.WriteObject(stream, View); #endif })); } catch (Exception e) { MessageBox.Show("ClipFlair options save failed: " + e.Message); } finally { View.Busy = false; //in any case (error or not) clear the Busy flag } if (SavedOptions != null) SavedOptions(this, null); //notify any listeners } //... } }

HowTo: load CaptionElements into Silverlight Media Framework player

Trying to make CaptionsGridWindow of ClipFlair serve captions editing on-the-fly to SMF (Silverlight Media Framework [now called MMPPF]) player component, I had a real hard time, plagued by a bug at TimedTextElementStyle. It seems to be setting default FontSize for captions using a “Cell” unit instead of using a “Pixel” unit. Currently SMF only supports “Pixel” units at FontSize of TimedTextElements according to the codedoc notes.

Another issue I had was that CaptionRegion constructor sets it to be active all the time (specifying a Begin value of a min possible TimeExtent and an End value of the max possible TimeExtent), so it renders ShowBackground.WhenActive setting useless for it. That is if you want the captions background to show up only when there are captions showing (active), then you have to set the CaptionRegion’s background to Colors.Transparent and set each CaptionElement’s background to some non-transparent color (note that both CaptionRegion and CaptionElement are TimedTextElements, with the later added to the former’s Children property, forming a TimedTree that is).

 

public void UpdateMarkers(MediaMarkerCollection<TimedTextElement> newMarkers)
 {
   if (newMarkers == null) return;

   CaptionRegion  region = new CaptionRegion();
   region.Style.ShowBackground = ShowBackground.WhenActive; 
//doesn't seem to work if other than transparent color is used region.Style.BackgroundColor = Colors.Transparent; foreach (CaptionElement marker in newMarkers) { region.Children.Add(marker); marker.CaptionElementType = TimedTextElementType.Text; marker.Style.ShowBackground = ShowBackground.WhenActive; marker.Style.BackgroundColor = Color.FromArgb(100, 0, 0, 0);
//use a semi-transparent background marker.Style.Color = Colors.White; //marker.Style.TextAlign = TextAlignment.Center; Length length = new Length { Unit = LengthUnit.Pixel, Value = 20 }; //must use this, since the default LengthUnit.Cell used
//at TimedTextStyle constructor is not supported
marker.Style.FontSize = length; } Captions.Add(region); }

HowTo: Scale control arround its center using a render transform

In the context of ClipFlair development, I was recently adding independent scaling (zooming) functionality to its ZUI container’s floating windows (apart from the container zooming functionality) and came across some strange behavior, where the windows seemed to also move apart from getting scaled.

After banging my head a bit I decided to take a closer look at Microsoft’s documentation and noticed the following text at http://msdn.microsoft.com/en-us/library/system.windows.uielement.rendertransformorigin.aspx

"RenderTransformOrigin has a somewhat nonstandard use of the Point structure value, in that the Point does not represent an absolute location in a coordinate system. Instead, values between 0 and 1 are interpreted as a factor for the range of the current element in each x,y axis. For example, (0.5,0.5) will cause the render transform to be centered on the element, or (1,1) would place the render transform at the bottom right corner of the element. NaN is not an accepted value. Values beyond 0 and 1 are also accepted, and will result in more unconventional transform effects. For instance, if you set RenderTransformOrigin to be (5,5), and then apply a RotateTransform, the rotation point will be well outside the bounds of the element itself. The transform will spin your element around in a big circle that originates beyond bottom right. The origin might be somewhere inside its parent element and could possibly be possibly out of frame or view. Negative point values are similar, these will go beyond the top left bounds. Render transforms do not affect layout, and are typically used to animate or apply a temporary effect to an element."

Since ClipFlair’s FloatingWindowHostZUI template uses a Canvas to host its FloatingWindows, I obviously didn’t care about the phrase “Render transforms do not affect layout”, but the phrase “values between 0 and 1 are interpreted as a factor for the range of the current element in each x,y axis” rang a bell immediately.

Misguided by the poor Intellisense info for the RenderTransformOrigin property, I had thought that point was in the control’s coordinate system, and since I wanted to scale the control arround its center, I had used the following erroneous statement:

window.RenderTransformOrigin = new Point(window.ActualWidth/2, window.ActualHeight/2);
window.RenderTransform = new ScaleTransform().SetScale((double)e.NewValue);

instead of the correct one:

window.RenderTransformOrigin = new Point(0.5, 0.5);
                                                        //scale arround the window center
window.RenderTransform = new ScaleTransform().SetScale((double)e.NewValue);

That is the range 0 to 1 for x & y coordinates of RenderTransformOrigin refers to the UIElement region, whereas less or greater values are (proportionally) outside of it, useful for example if you want to rotate an object arround an external point with a RotateTransform.

Don’t get puzzled by the expression new ScaleTransform().SetScale(…), it’s a syntax I use for authoring portable source code between WPF and Silverlight (since Silverlight only has a parameter-less constructor for ScaleTransform and anyway WPF doesn’t have a constructor that takes a single parameter for both X and Y scale values).

To sum up, here’s the “Scale” property I added to the FloatingWindow class:

#region public double Scale   
/// <summary> /// Gets or sets current window scale. /// </summary> /// <value>Current scale.</value>
public double Scale {  
get { return (double)GetValue(ScaleProperty); }
  set { SetValue(ScaleProperty, value); } } /// <summary>
/// Identifies the <see cref="FloatingWindow.Scale" /> dependency property.
/// </summary>
/// <value>
/// The identifier for the <see cref="FloatingWindow.Scale" /> dependency property.
/// </value>
public static readonly DependencyProperty ScaleProperty =
    DependencyProperty.Register("Scale", typeof(double), typeof(FloatingWindow),
      new PropertyMetadata(1d, OnScalePropertyChanged));
//Must use 1d here, not 1 (else will get XAMLParseException at runtime)
/// <summary>
/// ScaleProperty PropertyChangedCallback call back static function.
/// </summary>
/// <param name="d">FloatingWindow object whose Scale property is changed.</param>
/// <param name="e">DependencyPropertyChangedEventArgs contains old and new values.</param>
private static void OnScalePropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {  
FloatingWindow window = (FloatingWindow)d;
  if (window != null)
  { 
    window.RenderTransformOrigin = new Point(0.5, 0.5); //scale arround the window center
    window.RenderTransform = new ScaleTransform().SetScale((double)e.NewValue);
}
} #endregion

HowTo: Bind to a DataContext property named Source in XAML

While refactoring ClipFlair code to use MVVM (Model-View-ViewModel) pattern, I came across the XAML error “Object reference not set to an instance of an object.”, shown in Visual Studio when trying to bind to my ViewModel (accessed implicitly, being set as the DataContext of the XAML control) for a property named Source.

I have settled down to using the following MVVM-style pattern (of my own) at ClipFlair:

image

Update: Later on, at ClipFlair project, I renamed folder “Views” to “ViewModels” and folder “Components” to “Views” and moved “ViewModels” folder inside the “Views” one to keep view-related stuff in one place.

IImageViewer.cs:

image

ImageView.cs:

image

Update: Later on, I refactored this to initialize the fields directly instead of at the constructor:

//can set fields directly here or at the constructor     
private Uri source = IImageViewerDefaults.DefaultSource;

ImageViewerWindow.xaml.cs (the so-called codebehind for the XAML control):

image

Note above how we set the “DataContext” of the control to our ViewModel, so that we can bind to its properties implicitly (without referencing it) in the XAML.

Update: There’s something very important I had forgotten to do at the implementation of the “View” property in the code above, that is to listen for PropertyChangeEvents emitted by the ViewModel. In specific, we need to listen for changes of the ViewModel’s “Source” property to keep the View’s “SourceProperty” (a DependencyProperty that is also accessed via the View’s “Source” property) in sync with the ViewModel’s “Source” property. So the code shown in the above screenshot has to be fixed by changing the “View” property’s implementation like below:

image

For extra safety in case you want to allow setting a null ViewModel to the View property, you could check for (value != null) before trying to add PropertyChanged event handler to it:

if (value != null)

  value.PropertyChanged += new PropertyChangedEventHandler(View_PropertyChanged);

Obviously, if more DependencyProperties are added to the component (e.g. to be able to set them declaratively from XAML when instantiating the component) that have to be in sync to respective ones at the ViewModel (which is held by the View property), then the “View_PropertyChanged” method (event handler) implementation has to be extended with “else if” statements for each of those properties to set the respective dependency properties from the matching ViewModel properties.

BTW, if your ViewModel passes null to the changed property name to mark a single change event for multiple properties, then you have to also take that in mind (else you will get exception when you try to call “Equals” method on null string), by doing something like below:

if (e.PropertyName == null) {

Source = View.Source;

OtherProperty = View.OtherProperty;

//…sync all properties since we are not told which ones changed

return;

}

else if (e.PropertyName.Equals(“Source”) {

Source = View.Source;

}

else if (e.PropertyName.Equals(“OtherProperty”)

{

OtherProperty = View.OtherProperty;

}

//…

Note that instead of “.Equals” one could have also used “==” operator, since the C# compiler maps the string equality operator to string’s “.Equals” method.

Update: Another retouch I have since done to the code above is to avoid hardcoding property names and their default values and use nstead ithe proprerty name constants defined at the class IImageViewerProperties and the respective default values defined at IImageViewerDefaults. For example, I prefer to use (after adding a “using ClipFlair.Models.Views;” to the top):

public static readonly DependencyProperty SourceProperty =

DependencyProperty.Register(IImageViewerProperties.PropertySource, typeof(Uri), typeof(ImageWindow), new FrameworkPropertyMetadata(

(Uri)IImageViewerDefaults.DefaultSource,

new PropertyChangedCallback(OnSourceChanged)));

Update: I eventually opted for using a “switch” statement instead of nested if / else if statements, to keep things cleaner when more properties are added:

    protected void View_PropertyChanged(object sender, 
                                        PropertyChangedEventArgs e)
    {
       if (e.PropertyName == null)
       {
         Source = View.Source;
         //...
       }
       else switch (e.PropertyName)
           //string equality check in .NET uses ordinal (binary) comparison 
           //semantics by default
         {
           case IImageViewerProperties.PropertySource:
             Source = View.Source;
             break;
           //...
         }
     }

In an even newer iteration of that code, I removed the if/else and added the null check in the switch statement:

    protected void View_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
      switch (e.PropertyName) 
      {
        case null:
          Source = View.Source;
          //...
          break;
        case IImageViewerProperties.PropertySource:
          Source = View.Source;
          break;
        //...
       }
    }

ImageViewerWindow.xaml (the XAML control):

image

The issue at the last screenshot is that you can’t write Source=”{Binding Source}” as you’d write Source=”{Binding SomeDataContextProperty}”, but need to write Source=”{Binding Path=Source}” instead, since Source is a keyword at Binding expression syntax.

BTW, note that at the “edImageURL” TextBox I use Mode=TwoWay (two-way binding, the default is one way binding from source [the DataContext in our case] to target), whereas at the Image control I don’t. Both get an image URI from the ViewModel’s “Source” property, but we also want to be able to edit the URI at the textbox and update the view’s Source property (which will in turn update the Image control with the new image).

Also, note that apart from passing a URI to the Source property of an Image control there’s also a more verbose syntax one could use above, which can be handy to know of:

<Image>

<Image.Source>

<BitmapImage UriSource=”{Binding Path=Source}” />

</Image.Source>

</Image>

The <Image.Source> tag here is using the so-called property element syntax pattern. In the snippet above it sets a value to the Source property of the Image tag. XAML can use both XML attributes and child elements to set values to properties of controls, but whereas the attribute name would be “Source”, the child element tag has to be “Image.Source”, not just “Source”, since the XML schema needs to have unique definitions for the XML tags, whereas multiple objects could define different Source properties causing naming collisions.

According to http://10rem.net/blog/2012/03/27/tip-binding-an-image-elements-source-property-to-a-uri-in-winrt-xaml this more verbose syntax is needed at Windows 8 Metro-style apps (that is for the WinRT API) for binding an Image to a URI (but since that article is back from March 2012, probably this is not an issue any more).

%d bloggers like this: