Microsoft Visual Studio Installer Projects


I was forced to remain using Visual Studio 2010 and not upgrade to Visual Studio 2012 or Visual Studio 2013, because my company has dozens of internally used utility apps that use visual studio installer/setup projects. It just is not worth the effort to migrate to another installer technology.

Surprising, last year Microsoft reversed the decision to remove set-up projects, as politically the focus was on getting developers making Windows Store applications where the end product is submitted to Microsoft to host and manage. I write this post as I still come across developers that are unaware that this functionality has been returned.

You can download the extension package from here that updates Visual Studio 2013 only (not Visual Studio 2012)

Setup

After installing the extension you will see the project template available in Visual Studio 2013 shown below

Installer/Setup project

I am currently evaluating Visual Studio 2015 preview, so one naturally assumed that you would be able to create a new set-up project as the functionality had been returned in the previous version, albeit via an add-in extension, I assumed I could see the project template shown in the image above, but it was not there.

It turns out that Microsoft have changed their mind again, and decided not to include installer/setup project in Visual Studio 2015, article available here.

We’d like to thank you all for your comments on this UserVoice entry. We have been discussing the comments on InstallShield Limited Edition (ISLE) raised here with Flexera and we are currently working with them to address the top issues. At this stage we have no plans to include the former Visual Studio Setup Projects in future product versions but we will continue to work with Flexera and the community to ensure Visual Studio customers’ setup needs will be met with no-cost tooling that supports a broad range of scenarios.

Tony Goodhew, Program Manager, VS Pro.

 This is a very frustrating development, a decision based on political and not technical issues. it just means a whole class of Visual Studio developer simply won’t upgrade.

Writing multithreaded programs using Async & Await in C#


The biggest feature in C# and Visual Basic languages in Visual Studio 11 is inclusion of first class language support for asynchrony. Writing multi-threaded programs that scale, are easily modifiable, maintainable and understandable by more than one person (i.e. the one that wrote the code) is hard. This is certainly something I have seen people get wrong time and time again.

Probably the most dangerous word to use with undergraduate or even postgraduates is the word “Thread”. When developing software systems, this is certainly an area that requires somebody experienced, and proactive enough to ensure that developers are monitored when they are assigned tasks using threading, as things can get out of hand very quickly.. I have always tried to ensure that developers refrain from using the Thread class and suggesting they use the ThreadPool  and its QueueUserWorkItem  method or the background worker component where possible, but there are times when you have to implement the IAsyncResult design pattern, which makes understanding what your normally synchronous application (and mind-set) hard, because there is an Inversion of Control in the code that is executing, requiring you use a WaitCallBack and WaitHandles and ManualResetEvent and AutoResetEvent classes.

As we move forward, it is more important that developers are as productive as possible, especially in a world where devices are proliferating at the rate that they are. I can now program a Computer, Mobile Phone or Tablet, where connectivity, and updating is a real issue on tablets and phones, so being able to write robust, multi-threaded code quickly and relatively bug free increases in importance.

Note that I am using WPF in this example, if you would like to know how to do this in WinRT/Metro, then have a look at this example.

Single Threaded Example

If you run Visual Studio and create a new WPF application and call the project AsyncWpfApplication

NewProject

This synchronous application application is going to process a list of URLs, and calculate the size of the pages so please ensure you add a reference to System.Net.Http

SystemNet

Add the following XAML markup

<Window x:Class="AsyncWpfApplication.MainWindow"
Title="MainWindow" Height="400" Width="600">
<Grid>
 
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
 
<TextBox Margin="20" Grid.Row="0" HorizontalAlignment="Left" TextWrapping="Wrap" FontFamily="Lucida Console" x:Name="resultsTextBox" Height="250" Width="500" />
<Button Grid.Row="1" HorizontalAlignment="Right" Margin="0,0,70,20" Content="Start" x:Name="startButton" Height="22" Width="75" Click="startButton_Click_1" />
 
</Grid>
</Window>
 

Add the following in the code behind

using System;
using System.Collections.Generic;
using System.Windows;
using System.Net.Http;
using System.Net;
using System.IO;
using System.Threading.Tasks;
 
namespace AsyncWpfApplication
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
 
private void startButton_Click_1(object sender, RoutedEventArgs e)
{
resultsTextBox.Clear();
SumPageSizes();
resultsTextBox.Text += "\r\nControl returned to startButton_Click.";
}
 
private void SumPageSizes()
{
// Make a list of web addresses.
IEnumerable<string> urlList = SetUpURLList();
 
var total = 0;
foreach (var url in urlList)
{
// GetURLContents returns the contents of url as a byte array.
byte[] urlContents = GetURLContents(url);
 
DisplayResults(url, urlContents);
 
// Update the total.
total += urlContents.Length;
}
 
// Display the total count for all of the web addresses.
resultsTextBox.Text += string.Format("\r\n\r\nTotal bytes returned:  {0}\r\n", total);
}
 
 
private IEnumerable<string> SetUpURLList()
{
var urls = new List<string>
{
};
return urls;
}
 
 
private byte[] GetURLContents(string url)
{
// The downloaded resource ends up in the variable named content.
var content = new MemoryStream();
 
// Initialize an HttpWebRequest for the current URL.
var webReq = (HttpWebRequest)WebRequest.Create(url);
 
// Send the request to the Internet resource and wait for
// the response.
using (var response = webReq.GetResponse())
{
// Get the data stream that is associated with the specified URL.
using (Stream responseStream = response.GetResponseStream())
{
// Read the bytes in responseStream and copy them to content. 
responseStream.CopyTo(content);
}
}
 
// Return the result as a byte array.
return content.ToArray();
}
 
private void DisplayResults(string url, byte[] content)
{
// Display the length of each website. The string format
// is designed to be used with a monospaced font, such as
// Lucida Console or Global Monospace.
var bytes = content.Length;
// Strip off the "http://&quot;.
var displayURL = url.Replace("http://&quot;, "");
resultsTextBox.Text += string.Format("\n{0,-58} {1,8}", displayURL, bytes);
}
}
}
 

If you then hit F5 to run the application and press start you will find that the start button sticks and the application itself is unresponsive because of all the work that is blocking the main UI thread. After a few moment you should get the following

Screen

Multithreaded Example

When retrofitting the new asyc language features you need to be aware of the following

  1. The new asyc language capabilities are based around Task<T> found in System.Threading.Tasks.Task<TResult>. Knowing how that class works with increase the ease of you ability to use the new asyc features
  2. Any method that uses the new asynchronous functionality must use the new keyword asyc.
  3. There are a plethora of new classes in the .NET framework that are suffixed with Asyc that return a Task<T> that you can use with the new await keyword.
using System;
using System.Collections.Generic;
using System.Windows;
using System.Net.Http;
using System.Net;
using System.IO;
using System.Threading.Tasks;
 
namespace AsyncWpfApplication
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
 
private async void startButton_Click_1(object sender, RoutedEventArgs e)
{
resultsTextBox.Clear();
await SumPageSizesAsync();
resultsTextBox.Text += "\r\nControl returned to startButton_Click.";
}
 
private async Task SumPageSizesAsync()
{
// Make a list of web addresses.
IEnumerable<string> urlList = SetUpURLList();
 
var total = 0;
 
foreach (var url in urlList)
{
// GetURLContentsAsync returns a Task<T>. At completion, the task
// produces a byte array.
Task<byte[]> getContentsTask = GetURLContentsAsync(url);
byte[] urlContents = await getContentsTask;
 
// The following line can replace the previous two assignment statements.
//byte[] urlContents = await GetURLContentsAsync(url);
 
DisplayResults(url, urlContents);
 
// Update the total.         
total += urlContents.Length;
}
 
// Display the total count for all of the websites.
resultsTextBox.Text +=
string.Format("\r\n\r\nTotal bytes returned:  {0}\r\n", total);
}
 
 
 
 
private IEnumerable<string> SetUpURLList()
{
var urls = new List<string>
{
};
return urls;
}
 
private async Task<byte[]> GetURLContentsAsync(string url)
{
// The downloaded resource ends up in the variable named content.
var content = new MemoryStream();
 
// Initialize an HttpWebRequest for the current URL.
var webReq = (HttpWebRequest)WebRequest.Create(url);
 
// Send the request to the Internet resource and wait for
// the response.
Task<WebResponse> responseTask = webReq.GetResponseAsync();
using (WebResponse response = await responseTask)
{
// The following line can replace the previous two lines.
//using (WebResponse response = await webReq.GetResponseAsync())
 
// Get the data stream that is associated with the specified url.
using (Stream responseStream = response.GetResponseStream())
{
// Read the bytes in responseStream and copy them to content.
// CopyToAsync returns a Task, not a Task<T>.
Task copyTask = responseStream.CopyToAsync(content);
 
// When copyTask is completed, content contains a copy of
// responseStream.
await copyTask;
 
// The following line can replace the previous two statements.
//await responseStream.CopyToAsync(content);
}
}
 
// Return the result as a byte array.
return content.ToArray();
}
 
 
 
private void DisplayResults(string url, byte[] content)
{
// Display the length of each website. The string format
// is designed to be used with a monospaced font, such as
// Lucida Console or Global Monospace.
var bytes = content.Length;
// Strip off the "http://&quot;.
var displayURL = url.Replace("http://&quot;, "");
resultsTextBox.Text += string.Format("\n{0,-58} {1,8}", displayURL, bytes);
}
}
}

    If you examine the retrofitted example above you will see that the synchronous methods have indeed been replaced with their methods that are suffixed with Async e.g. SomeMethodAsyc and all you have to do is use the await keyword until the asynchronous task completes. The C# language team have really made asynchronous programming “easy peasy lemon squeezy”.

Single instance WPF application in C#


Update: 12/03/2012

I ran into some issues with the approach that I linked to below, and would not recommend it. It turns out that the best way to deal with this issue, is to use the Visual Basic instance detection classes (yes you can use these from C#). The sample code is available here

Note that is is more than likely that you will be retrofitting this to an existing project. If that is the case, make sure you double click the properties pane in visual studio and set your custom start-up file/class as shown below

startup

 

Original Post

Keeping in the same vein as my previous post, I frequently find that I have to solve the same problems at times. After a year or several months I usually move onto another project, consequently no longer have the source code, so I am posting this as a bit of a sticky as I am sure someone will find it useful.

Visual Basic has single instance classes one can use, one can import these into any C# application and consume them (that, after all,  is the real beauty of .NET), but I found that there was too much code, and too many classes, and it was taking me too long a little while back. I solve this problem using the Process class. Create a WPF application and use as follows;

using System.Diagnostics;
using System.Linq;
using System.Windows;
 
namespace SingleInstanceApp
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
IsAppAlreadyRunning();
}
 
private static void IsAppAlreadyRunning()
{
Process currentProcess = Process.GetCurrentProcess();
 
if (Process.GetProcessesByName(currentProcess.ProcessName).Any(p => p.Id != currentProcess.Id))
{
MessageBox.Show("Another instance is already running.", "Application already running",
MessageBoxButton.OK, MessageBoxImage.Exclamation);
Current.Shutdown();
return;
}
}
}
}

Copying an object in C# and Visual Basic


Sometimes there are classes you find you use in every project. I remember once (shudder) I, rather inelegantly instantiated a class, and performed a manual copy of all the properties in the object (when the pressure was on, and I just needed to make the thing work).

I have also found myself spending hours chasing subtle and hard to debug issues where ICloneable has been used -  which, incidentally, is widely accepted as an interface one ought never use – or Object.MemberwiseClone. It turns out the only way to do this correctly, is to serialise the object, and then use the deserialised object, implemented here using an extension method.

Even though this is quite straightforward, I still am posting this however , because I find I need to use this functionality frequently, and often I no longer have access to the source, or it takes ages to find the class. The Visual Basic code sample is after the C# one.

C#

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
 
namespace ObjectCopierApp.Extensions
{
/// <summary>
/// Provides a method for performing deep copying of an object.
/// Binary Serialization is used to perform the copy.
/// </summary>
internal static class ObjectCopier
{
/// <summary>
/// Perform a deep copy of an object.
/// </summary>
/// <typeparam name="T">The type of object being copied.</typeparam>
/// <param name="source">The object instance to copy.</param>
/// <returns>The copied object.</returns>
internal static T Clone<T>(this T source)
{
if (!typeof(T).IsSerializable)
{
throw new ArgumentException("The type must be serializable.", "source");
}
 
// Don’t serialize a null object, simply return the default for that object
if (ReferenceEquals(source, null))
{
return default(T);
}
 
IFormatter formatter = new BinaryFormatter();
 
using (var stream = new MemoryStream())
{
formatter.Serialize(stream, source);
stream.Seek(0, SeekOrigin.Begin);
return (T)formatter.Deserialize(stream);
}
}
}
}
 
 

Visual Basic

Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary
 
Namespace ObjectCopierApp.Extensions
”’ <summary>
”’ Provides a method for performing deep copying of an object.
”’ Binary Serialization is used to perform the copy.
”’ </summary>
Friend NotInheritable Class ObjectCopier
Private Sub New()
End Sub
”’ <summary>
”’ Perform a deep copy of an object.
”’ </summary>
”’ <typeparam name="T">The type of object being copied.</typeparam>
”’ <param name="source">The object instance to copy.</param>
”’ <returns>The copied object.</returns>
<System.Runtime.CompilerServices.Extension()> _
Friend Shared Function Clone(Of T)(source As T) As T
If Not GetType(T).IsSerializable Then
Throw New ArgumentException("The type must be serializable.", "source")
End If
 
‘ Don’t serialize a null object, simply return the default for that object
If ReferenceEquals(source, Nothing) Then
Return Nothing
End If
 
Dim formatter As IFormatter = New BinaryFormatter()
 
Using stream = New MemoryStream()
formatter.Serialize(stream, source)
stream.Seek(0, SeekOrigin.Begin)
Return DirectCast(formatter.Deserialize(stream), T)
End Using
End Function
End Class
End Namespace

ItemsControl Performance Improvements in .NET 4.5


WPF is Slow

One of the most common complaints from C++ or Windows Forms developers moving to WPF/XAML is the issue of performance. I have worked exclusively and intricately with WPF/XAML for the last 4 years and at times have come across performance issues that put me in a corner, including;

  • I needed to enumerate all the alarm and safety devices in an Airports security system inventory. This included fire, smoke, travelator, escalator, motion detection etc. in effect I needed to load up thousands of hardware devices into the WPF application for configuration
  • I was dealing with scientific data for DNA, RNA and Protein. Typically you would have an image with an electropherogram attached to it, which contains thousands of points of floating point data. Scientists required the ability to step through several different samples quickly in order to perform analysis on this data.
    In both these applications, I ended up having to make significant compromises, including using a Windows Forms chart in the scientific application as loading thousands of floating point data into the plethora of commercial and open source WPF charts we tried was unbearably slow. The Windows Forms chart would render the data in milliseconds, where WPF would take a minimum of 10 or 20 seconds with the same data-set, and given the fact that we needed to extend the chart with additional functionality, it added several months worth of development to alter the Windows Forms chart, where it would have been far easier and less time consuming using WPF.

    Staggering

    It turns out that peoples cries about the sluggish nature of WPF applications are correct. Microsoft have managed to attain simply staggering performance improvements to the ItemsControl in WPF. An ItemsControl represents a control that can be used to present a collection of items. In WPF, this includes the TreeView, ListBox, ListView and DataGrid controls that are built using ItemsControls.

DotNet4

In .NET 4 (Visual Studio 2010) and previous, a test was conducted where 12 000 items are loaded into an ItemsControl, the results are in the image below

OutOfMemory

After about 7 minutes, the computer throws an out of memory exception, which I am sure you will agree is absolutely terrible. Microsoft prioritised this issue and in the first iteration managed to increase the total number of items loaded to 200 000 (from 12 000) and load these in 24.5 seconds, an incredible improvement.

FirstIteration

The Pièce de résistance is that they continued to try and get this already considerable metric down even further, in the end  they got this down to 2.3 seconds.

Final

If you have a WPF application that is data centric, especially handling thousands of rows of data, then upgrading to .NET 4.5 is an absolute no brainer. WPF is now at least 182 times quicker whilst handling 16 times more data.

It means that WPF is going to be significantly faster with the additional improvements that have been made to virtualisation in ItemsControls and Cold/Warm start-up times for .NET applications in general.

Calculate range/limit with C#


A recurring requirement in every day programming is determining whether a value exists with a certain range. Since it is exam time (for most students at present), I resolved to write a quick application demonstrating this requirement based around examination marks.

This quick demo is written in WPF, but should work in Silverlight or Windows Phone 7 with no alteration. The Range class though, can be used throughout .NET in general.

To start with, create a new WPF application in visual Studio and call it ExamApp. Add a new folder to the project called Range and in this declare a new interface called IRange with the following members

using System;
 
namespace ExamApp.Range
{
public interface IRange<T> where T : IComparable<T>
{
T Start { get; }
T End { get; }
bool InRange(T valueToFind);
}
}

Create a new class in the same folder called Range which implements the interface above

using System;
 
namespace ExamApp.Range
{
public class Range<T> : IRange<T> where T : IComparable<T>
{
private readonly T start;
private readonly T end;
 
public Range(T start, T end)
{
if (start.CompareTo(end) <= 0)
{
this.start = start;
this.end = end;
}
else
{
this.start = end;
this.end = start;
}
}
 
public T Start
{
get { return this.start; }
}
 
public T End
{
get { return this.end; }
}
 
public bool InRange(T valueToFind)
{
return valueToFind.CompareTo(Start) >= 0 && valueToFind.CompareTo(End) <= 0;
}
}
}
 

Add a new Folder to the project called Examinations and in this add a new class called Percentage. This Percentage class inherits off Range, and here you can create whatever logical range class you may desire for your own applications

using System;

using ExamApp.Range;
 
namespace ExamApp.Examinations
{
public class Percentage<T> : Range<T> where T : IComparable<T>
{
public Percentage(T start, T end) : base(start, end)
{
}
}
}
 

Add a new enumeration called Grade thus

namespace ExamApp.Examinations

{
public enum Grade
{
None = 0,
A = 1,
B = 2,
C = 3,
D = 4,
E = 5,
} ;
}
 

Add a new interface called ITest with the following members.

namespace ExamApp.Examinations

{
interface ITest
{
int ArtAndDesignMark { get; set; }
int ScienceMark { get; set; }
int MathsMark { get; set; }
}
}
 

Add a new folder called Converters to the project, and add the following

using System;

using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
using System.Windows.Markup;
using ExamApp.Examinations;
 
namespace ExamApp.Converters
{
public class MarkToGradeConverter : MarkupExtension, IValueConverter
{
private static MarkToGradeConverter instance;
private static readonly Dictionary<Percentage<int>, Grade> Marks;
 
static MarkToGradeConverter()
{
Marks = new Dictionary<Percentage<int>, Grade>
{
{new Percentage<int>(80, 100), Grade.A},
{new Percentage<int>(75, 79), Grade.B},
{new Percentage<int>(60, 74), Grade.C},
{new Percentage<int>(55, 59), Grade.D},
{new Percentage<int>(0, 54), Grade.E},
};
}
 
public override object ProvideValue(IServiceProvider serviceProvider)
{
return instance ?? (instance = new MarkToGradeConverter());
}
 
#region Implementation of IValueConverter
 
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return GetGrade((int)value).ToString();
}
 
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
 
public static Grade GetGrade(int mark)
{
Percentage<int> key = Marks.Keys.FirstOrDefault(r => r.InRange(mark));
 
if (key != null)
{
Grade grade;
 
if (Marks.TryGetValue(key, out grade))
{
return grade;
}
}
return Grade.None;
}
 
#endregion
}
}
 

As you can see most of the logic is executed here. Add a new class to the project called Student with the following properties. Also note that this implements the ITest interface that was declared earlier

using ExamApp.Examinations;

 
namespace ExamApp
{
public class Student : ITest
{
public Student(string firstName, string surname, int artAndDesignMark, int scienceMark, int mathsMark)
{
this.FirstName = firstName;
this.Surname = surname;
this.ArtAndDesignMark = artAndDesignMark;
this.ScienceMark = scienceMark;
this.MathsMark = mathsMark;
}
 
public Student()
{
}
 
public string FirstName { get; set; }
public string Surname { get; set; }
 
#region Implementation of ITest
 
public int ArtAndDesignMark { get; set; }
public int ScienceMark { get; set; }
public int MathsMark { get; set; }
 
#endregion
 
}
}
 

We need a collection of students to show in a list so create the following class that inherits off ObservableCollection<T>

using System.Collections.ObjectModel;

namespace ExamApp
{
public class Students : ObservableCollection<Student>
{
public Students()
{
Add(new Student
{
FirstName = "John",
Surname = "Lennon",
ArtAndDesignMark = 81,
ScienceMark = 59,
MathsMark = 77
});
Add(new Student
{
FirstName = "Paul",
Surname = "McCartney",
ArtAndDesignMark = 88,
ScienceMark = 40,
MathsMark = 66
});
Add(new Student
{
FirstName = "Ringo",
Surname = "Starr",
ArtAndDesignMark = 88,
ScienceMark = 31,
MathsMark = 96
});
Add(new Student
{
FirstName = "George",
Surname = "Harrison",
ArtAndDesignMark = 100,
ScienceMark = 99,
MathsMark = 99
});
 
}
}
}
 

Now the data structures are complete, add the following .xaml markup

<Window x:Class="ExamApp.MainWindow"
xmlns:Converters="clr-namespace:ExamApp.Converters"
Title="MainWindow"
Width="525"
Height="350">
<Window.Resources>
<Converters:MarkToGradeConverter x:Key="MarkToGradeConverter" />
</Window.Resources>
<ListView x:Name="studentsListBox">
<ListView.View>
<GridView>
<GridViewColumn Width="120"
DisplayMemberBinding="{Binding Path=FirstName}"
Header="Name" />
<GridViewColumn Width="120"
DisplayMemberBinding="{Binding Path=Surname}"
Header="Surname" />
<GridViewColumn Width="40"
DisplayMemberBinding="{Binding Path=ArtAndDesignMark,
Converter={StaticResource MarkToGradeConverter}}"
Header="Art" />
<GridViewColumn Width="60"
DisplayMemberBinding="{Binding Path=ScienceMark,
Converter={StaticResource MarkToGradeConverter}}"
Header="Science" />
<GridViewColumn Width="50"
DisplayMemberBinding="{Binding Path=MathsMark,
Converter={StaticResource MarkToGradeConverter}}"
Header="Maths" />
</GridView>
</ListView.View>
 
</ListView>
 
</Window>
 

Add the following code to the constructor of the main window

using System.Windows;
 
namespace ExamApp
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.studentsListBox.ItemsSource = new Students();
}
}
}
 

You should have the following

Students

As you can see it is quite a simple and pretty reusable way to use ranges in your application.

The source code for this simple example is available here

Zoomable autosizing canvas in WPF


I have created a control for a project I am working on that uses a canvas to draw graph ticks for a custom scale, but ran into the limitation of the canvas control where child items are not scaled accordingly when the height (or width) of the canvas changes. Luckily, this is quite a straightforward problem to fix, but I just could not seem to find either the correct search terms to enter in Google or locating code samples in books that demonstrated how to resolve the issue. I have just knocked this code up this afternoon so it is not perfect, but should suffice should you require something similar.

Small

Large

In order to resolve the problem you will need to create a custom canvas and override both MeasureOverride and ArrangeOveride incorporating any custom logic you require in these methods.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
 
namespace AutosizingCanvasApp.Controls
{
public class TickCanvas : Canvas
{
// Vertical axis where the X value is never changed
private const double X = 0;
private Size initialSize;
 
// Override the default Measure method of Panel
protected override Size MeasureOverride(Size availableSize)
{
var canvasDesiredSize = new Size();
 
// In our example, we just have one child.
// Report that our canvas requires just the size of its only child.
 
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
canvasDesiredSize = child.DesiredSize;
}
 
return canvasDesiredSize;
}
 
protected override Size ArrangeOverride(Size finalSize)
{
if (initialSize.Height == 0)
{
initialSize = finalSize;
}
var ratio = finalSize.Height / initialSize.Height;
 
for (int index = 0; index < this.InternalChildren.Count; index++)
{
UIElement child = this.InternalChildren[index];
 
double y = ((Line)child).Y1;
 
child.Arrange(finalSize.Height > initialSize.Height
? new Rect(new Point(X, (ratio * y) – y), child.DesiredSize)
: new Rect(new Point(X, 0), child.DesiredSize));
}
return finalSize; // Returns the final Arranged size
}
}
}

In the .xaml I have a border with the custom canvas as a child that have some Line objects all affecting the Y axis position.

<Window x:Class="AutosizingCanvasApp.MainWindow"
xmlns:controls="clr-namespace:AutosizingCanvasApp.Controls"
xmlns:local="clr-namespace:AutosizingCanvasApp.Converters"
Title="mainWindow"
Width="300"
Height="768"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<local:BorderHeightValueConverter x:Key="BorderHeightValueConverter" />
<local:LinePositionConverter x:Key="LinePositionConverter" />
</Window.Resources>
 
<Border x:Name="mainBorder"
Width="94"
Height="{Binding ElementName=mainWindow,
Path=ActualHeight,
Converter={StaticResource BorderHeightValueConverter}}"
Margin="50"
BorderBrush="Black"
BorderThickness="2"
ToolTip="{Binding ElementName=mainBorder,
Path=ActualHeight}">
<controls:TickCanvas x:Name="myCanvas">
<Line x:Name="line1"
Stroke="Red"
StrokeThickness="2"
ToolTip="{Binding ElementName=line1,
Path=Y1}"
X1="0"
X2="60"
Y1="{Binding ElementName=mainBorder,
Path=ActualHeight,
Converter={StaticResource LinePositionConverter},
ConverterParameter=0.2}"
Y2="{Binding ElementName=mainBorder,
Path=ActualHeight,
Converter={StaticResource LinePositionConverter},
ConverterParameter=0.2}" />
<Line x:Name="line2"
Stroke="Blue"
StrokeThickness="2"
ToolTip="{Binding ElementName=line2,
Path=Y1}"
X1="0"
X2="60"
Y1="{Binding ElementName=mainBorder,
Path=ActualHeight,
Converter={StaticResource LinePositionConverter},
ConverterParameter=0.4}"
Y2="{Binding ElementName=mainBorder,
Path=ActualHeight,
Converter={StaticResource LinePositionConverter},
ConverterParameter=0.4}" />
<Line x:Name="line3"
Stroke="Green"
StrokeThickness="2"
ToolTip="{Binding ElementName=line3,
Path=Y1}"
X1="0"
X2="60"
Y1="{Binding ElementName=mainBorder,
Path=ActualHeight,
Converter={StaticResource LinePositionConverter},
ConverterParameter=0.6}"
Y2="{Binding ElementName=mainBorder,
Path=ActualHeight,
Converter={StaticResource LinePositionConverter},
ConverterParameter=0.6}" />
<Line x:Name="line4"
Stroke="Purple"
StrokeThickness="2"
ToolTip="{Binding ElementName=line4,
Path=Y1}"
X1="0"
X2="60"
Y1="{Binding ElementName=mainBorder,
Path=ActualHeight,
Converter={StaticResource LinePositionConverter},
ConverterParameter=0.8}"
Y2="{Binding ElementName=mainBorder,
Path=ActualHeight,
Converter={StaticResource LinePositionConverter},
ConverterParameter=0.8}" />
</controls:TickCanvas>
</Border>
</Window>

I also have a couple of converters that converts the height of the border as that changes when you resize the parent window and a couple of converters that reposition the line objects in the canvas. Be aware that the border itself has a thickness, so you will need to incorporate that in any sizing but has been omitted here so the lines may be a couple of pixels off.

using System;
using System.Globalization;
using System.Windows.Data;
 
namespace AutosizingCanvasApp.Converters
{
public class LinePositionConverter : IValueConverter
{
#region Implementation of IValueConverter
 
/// <summary>
/// Converts a value.
/// </summary>
/// <returns>
/// A converted value. If the method returns null, the valid null value is used.
/// </returns>
/// <param name="value">The value produced by the binding source.</param>
/// <param name="targetType">The type of the binding target property.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((double) value)*double.Parse(parameter.ToString());
}
 
/// <summary>
/// Converts a value.
/// </summary>
/// <returns>
/// A converted value. If the method returns null, the valid null value is used.
/// </returns>
/// <param name="value">The value that is produced by the binding target.</param><param name="targetType">The type to convert to.</param><param name="parameter">The converter parameter to use.</param><param name="culture">The culture to use in the converter.</param>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
 
#endregion
}
}

using System;
using System.Globalization;
using System.Windows.Data;
 
namespace AutosizingCanvasApp.Converters
{
public class BorderHeightValueConverter : IValueConverter
{
#region Implementation of IValueConverter
 
/// <summary>
/// Converts a value.
/// </summary>
/// <returns>
/// A converted value. If the method returns null, the valid null value is used.
/// </returns>
/// <param name="value">The value produced by the binding source.</param>
/// <param name="targetType">The type of the binding target property.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (((double)value)*0.7);
}
 
/// <summary>
/// Converts a value.
/// </summary>
/// <returns>
/// A converted value. If the method returns null, the valid null value is used.
/// </returns>
/// <param name="value">The value that is produced by the binding target.</param><param name="targetType">The type to convert to.</param><param name="parameter">The converter parameter to use.</param><param name="culture">The culture to use in the converter.</param>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
 
#endregion
}
}
 

The source code is for this example is available here.