“...the truth is that those who reluctantly cast their vote for Trump were far outnumbered by those who did so with a kind of gleeful rage. They may cite the Supreme Court or low taxes, but it’s Trumpism that they love, a politics unconstrained not only by rules or laws but by basic human civility.

When Trump tosses around childish insults and acts as though any American who doesn’t support him is his enemy, they don’t say, ‘I don’t like that; it’s the other stuff I like.’ Trump’s vulgarity and hatefulness is exactly what they like. It’s a feature, not a bug. Seeing a political leader who enacts their darkest impulses on a daily basis thrills and intoxicates them.”

— Paul Waldman in The Washington Post

WPF Notes

UI framework notes — WPF 4.5 — XAML 2006

WPF Notes

These are my WPF notes, covering WPF 4.5 and XAML 2006. If you find a mistake, please let me know.

Most example code follows the Split Notation.

This page includes a two-column print style. For best results, print in landscape, apply narrow margins, change the Scale setting in your browser’s print options to 70%, and enable background graphics. Firefox and Chrome each have their own set of printing bugs, but one of them usually works.

Contents

These aren't finished, but I haven't been using WPF lately, so it may be a while before I get back to them.

XAML

XAML (Extensible Application Markup Language) is an XML-derived language that documents the configuration of objects and object hierarchies, particularly UI controls. Most XAML elements represent object instances that will be instantiated at run time by calling default constructors. The relative structure of XAML elements defines parent-child relationships among the objects. The attributes for a given element often represent values to be assigned to the object's properties:

<Window
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/
    presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  x:Class="nMain.tqWinCd" Title="Enter code">

  <StackPanel>
    <TextBox Name="qEdCd"/>
    <Button Name="qBtnOK" Click="BtnOK_Click" Content="OK"/>
  </StackPanel>
</Window>

The newest version is XAML 2009, which can be read manually with the System.Xaml assembly introduced in .NET 4.0. The Visual Studio XAML Designer only supports XAML 2006, as do the code-behind classes produced by Visual Studio.

It is possible to instantiate generic classes with XAML, and, when this is done, the x:TypeArguments attribute is used to specify type arguments for the generic. In XAML 2006, only root elements can be generic. To use a generic class elsewhere, create a subclass that passes type arguments to the generic in its inheritance list, and reference that in the XAML.

Namespaces

An XML namespace is declared by assigning a Uniform Resource Identifier (URI) to the xmlns attribute. In XML, namespaces are used to resolve name conflicts, which occur when data from different sources use the same name in the same file in different ways. In XAML, they are used to associate element and attribute names with classes and class properties in some library, particularly the .NET Framework. This namespace, for example, allows elements like Window and Button to be mapped to their classes:

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

In this case, the URI does not represent a web resource; it is merely a unique name that is known to the WPF assemblies. The XAML root element must declare at least one namespace so the root itself can be created. Namespaces declared in one element are usable in that element and in its children.

Appending a colon and an alternative name:

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

produces a prefix that can be attached to names in the XAML:

<Button x:Name="BtnStop" Content="Stop"/>

WPF uses assembly attributes to associate namespaces with URIs. Assemblies that were not designed for WPF will not have the necessary attribute, but they can be used in XAML by prefixing the .NET namespace with clr-namespace:

xmlns:coll="clr-namespace:System.Collections;assembly=mscorlib"

The assembly name must be appended to the namespace unless the .NET namespace happens to reside in the assembly containing the XAML.

Type converters

All XAML attribute values are strings. Most properties have non-string types, so .NET defines type converters that convert strings to other types. In this example:

<Button x:Name="BtnReady" Content="Ready" Background="Gray"/>

the BrushConverter class converts the Background attribute to a Brush instance. This is equivalent to:

BtnReady.Background = System.Windows.Media.Brushes.Gray;

Type conversion is enabled in the backing class by adding the TypeConverter attribute to the property itself, or to the type that is stored in the property. Custom type converters can be created by subclassing the TypeConverter class.

Property elements

If the type converter for a given property produces a parent class that is overly generic, or if it is necessary to initialize the object, the property can be set with a property element. This is a sub-element with a name that references the containing type, suffixed with the name of the targeted property. In this example, Button.Background is a property element:

<Button x:Name="BtnReady" Content="Ready">
  <Button.Background>
    <LinearGradientBrush>
      <GradientStop Color="LightGray" Offset="0.0"/>
      <GradientStop Color="Gray" Offset="1.0"/>
    </LinearGradientBrush>
  </Button.Background>
</Button>

This example constructs a LinearGradientBrush instance, configures it in the XAML, and assigns it to the button's Background property.

Markup extensions

Complex objects and special values like null can be assigned to properties with markup extensions. To invoke a markup extension, place the name of the extension class and zero or more comma-delimited parameters within curly braces. This example invokes the NullExtension class with no arguments:

BorderBrush="{x:Null}"

Markup extensions can accept positional parameters or named parameters. Positional parameters correspond to string arguments in an extension class constructor. In this example:

Content="{x:Static local:tWinMain.sTextAct}"

the string "local:tWinMain.sTextAct" is passed to the StaticExtension constructor.

Named parameters are name/value pairs that assign values to properties in the class. The value can itself invoke a type converter or another markup extension:

Content="{Binding Path=Title}"

Markup extension classes can also be used as values for property elements:

<Button.BorderBrush>
  <x:Null/>
</Button.BorderBrush>
<Button.Content>
  <x:Static Member="local:tWinMain.sTextAct"/>
</Button.Content>

When this is done, all parameters must be named.

Markup extensions are created by subclassing MarkupExtension. It is customary for the name of such a class to end with Extension, and, when this is done, the word Extension can be omitted from the class name in XAML.

Children of object elements

An element representing an object can contain a child element that is:

  • A type converter value;
  • A Content value;
  • A collection instance.

If a type converter can convert the contained string to an instance of the containing type, that converter will be invoked. In this example, a converter creates a Cursor instance and assigns it to the Cursor property:

<Button.Cursor>Hand</Button.Cursor>

with this being is equivalent to:

Cursor="Hand"

Many classes use an attribute to designate one property as the content property, with this often (but not always) being named Content. Like an implicit property element, this supports the assignment of complex objects that could not be defined in an attribute:

<Button>
  <Image Source="IconPrev.png"/>
</Button>

Element children can also be used to populate a collection. If the collection implements IList, and if an empty collection instance has already been assigned to the property (as all WPF controls do by default), then the contained instances will be added:

<ComboBox x:Name="BoxDef">
  <ComboBox.Items>
    <ComboBoxItem>Start</ComboBoxItem>
    <ComboBoxItem>Restart</ComboBoxItem>
  </ComboBox.Items>
</ComboBox>

If the collection property happens to be the object's content property, it can be assigned without a property element:

<ComboBox x:Name="BoxDef">
  <ComboBoxItem>Start</ComboBoxItem>
  <ComboBoxItem>Restart</ComboBoxItem>
</ComboBox>

If the collection property does not already reference an instance, the instance can be defined and then populated in the XAML.

If the collection implements IDictionary, the members must be associated with keys. Instead of wrapping each value instance with another class, this is done with the XAML Key keyword:

<Application.Resources>
  <Image x:Key="Begin" Source="IconBegin.png"/>
  <Image x:Key="Other" Source="IconOther.png"/>
</Application.Resources>

Element names

Many WPF classes derive from FrameworkElement or FrameworkContentElement, both of which implement FindName. This method can be used to locate and reference instances generated by the XAML:

var oqEdRank = oqWin.FindName("EdRank") as TextBox;

Names can be assigned to XAML elements in one of two ways. The FrameworkElement and FrameworkContentElement classes both define a Name property:

Name="EdRank"

In other classes, the name can be assigned with the XAML Name keyword:

x:Name="EdRank"

Both produce the same functionality, but only one should be used for a given instance.

Names can also be referenced from other parts of the XAML. For example, the Binding markup extension uses the element name to support data binding:

Target="{Binding ElementName=EdRank}"

Certain type converters also use the name:

Target="EdRank"

Using XAML

In Visual Studio, adding a new window unit produces two files: a XAML file, and a C# code-behind file with the xaml.cs extension. The code-behind is nested under the XAML file in the Solution Explorer. It contains a partial class that derives from Window:

namespace nMain {
  partial class tWin: Window {
    public tWin() {
      InitializeComponent();
    }
    ...

In general, a code-behind class must be a subclass of the XAML element type.

The Window element in the XAML includes a Class attribute that references the code-behind class:

x:Class="nMain.tWin"

At build time, XAML is compiled into a compact form called BAML (Binary Application Markup Language) that is stored as a resource in the containing assembly. The build also generates a C# file with the g.cs extension within obj/Debug or obj/Release. This file completes the partial definition of the code-behind class:

namespace nMain {
  partial class tWin: Window, IComponentConnector {
    internal System.Windows.Controls.Button BtnReady;

    public void InitializeComponent() {
      ...

    public void IComponentConnector.Connect(int aIDConn, object
      aqTarg) {
      ...

In particular, it implements the InitializeComponent method that is called from the constructor in the code-behind file. This method reads the resource BAML and instantiates the objects. Note that event handlers are attached to objects before properties are set, so that handlers can respond to property changes during construction. The generated definition also creates references for all named objects (including controls) contained by the window. References are associated with instances in the Connect method, which is called as the BAML is interpreted. The references are internal by default. To use a different access level, add x:FieldModifier attributes to the elements.

The code-behind class is assumed to be public. All partial definitions must have the same access level, so the generated definition is made public by default. To change the access level of the generated definition, add the x:ClassModifier attribute to the root node.

If WPF is used with a language without partial classes, a Subclass attribute can be added to the Window element. The generated class will take its name from Class, and the code-behind class will inherit from that class, while taking its name from Subclass.

XAML can also be read or written manually with the XamlReader and XamlWriter classes. To instantiate the structure from a XAML stream:

Window cWinFromXAML(Stream aqStm) {
  var oqWin = XamlReader.Load(aqStm) as Window;
  ...

WPF Infrastructure

WPF (Windows Presentation Foundation) is a framework that produces themable, resolution-independent user interfaces. It is rendered with Direct3D. Most WPF classes derive from the following 'core' classes:

WPF base classes

DispatcherObject manages a work queue for the UI thread, somewhat like the message loop in a Win32 application. Aside from 'frozen' instances of the Freezable class, instances should not be accessed from outside the UI thread. With this in mind, DispatcherObject includes a Dispatcher property that returns a Dispatcher instance. This class provides Invoke and BeginInvoke methods that run delegates on the UI thread.

DependencyObject supports dependency properties, covered below.

Visual is used to implement 2D controls. UIElement adds layout, control focus, event routing, and command binding functionality. FrameworkElement adds resource, data binding, and style support, along with tooltip and context menu functionality. Control adds additional styling features.

Visual3D and UIElement3D support 3D controls.

ContentElement offers many of the functions in UIElement, but it is used to create text elements, so it also supports text flow and wrapping. FrameworkContentElement adds some of the functionality in FrameworkElement. Neither class renders itself, so instances must be associated with one of the Visual classes.

Freezable classes support frozen and unfrozen states. When frozen, instances become read-only and (unlike other WPF instances) accessible to multiple threads. This class is typically used to implement graphics primitives.

In WPF, reference is sometimes made to logical and visual trees. A logical tree is a parent/child hierarchy that shows the instances explicitly created by the developer, whether in XAML or procedural code. A visual tree shows the instances that render those elements. Many logical tree instances will also be found in the visual tree, but some will not, and many visual tree instances will be absent from the logical tree, since they are created and contained by the logical instances.

Dependency properties

A dependency property is a .NET property backed by a DependencyProperty instance:

class tPanStat: Control {
  public static readonly DependencyProperty sCkEdProperty
    = DependencyProperty.Register(
        "CkEd",
        typeof(bool),
        typeof(tPanStat),
        new PropertyMetadata(false)
      );

  public bool CkEd {
    get { return (bool)GetValue(sCkEdProperty); }
    set { SetValue(sCkEdProperty, value); }
  }
  ...

This is used to implement data binding, UI styles, and animations, among other things.

The DependencyProperty is expected to be public and static, its name must end with Property, and it should be readonly. It is instantiated with one of the static Register methods, which accept an instance of PropertyMetadata or one of its subclasses. Metadata can be used to:

  • Specify a default value for the property;
  • Pass a callback that coerces input values to a valid range;
  • Pass a callback that validates input, throwing if it is invalid;
  • Pass a callback that responds to property changes;
  • Configure other behavior, such as property value inheritance.

Metadata can be changed in a subclass by calling OverrideMetadata from the property instance within the subclass's static constructor.

The .NET property is known as the property wrapper. The getter passes the DependencyProperty to GetValue, while the setter passes it to SetValue; these methods are implemented by DependencyObject, from which the containing class must derive. The property wrapper is called by the XAML compiler, but WPF calls GetValue and SetValue directly, so the wrapper should do nothing more than call those functions.

Value Precedence

When a dependency property is read, possible values are drawn from many sources. WPF starts by finding the base value, which it selects from the object tree. In order of precedence, this value is one of:

  1. The local value, produced by writing to the dependency property with its wrapper or with SetValue. A local value can be removed by passing the DependencyProperty instance to the ClearValue function in DependencyObject.
  2. The parent template value. A template is used to replace the object tree that implements a given control. If the dependency property is attached to an element within such a tree, the value may be drawn from the control that uses the template, or from a trigger attached to that control.
  3. The style value. A style is a collection of property values that can be applied to multiple control instances. The value can be drawn from triggers attached to the style, triggers attached to templates within the style, or from style setters.
  4. The default style or theme style value. Every WPF control has a default style that defines its basic appearance. Because this style varies with the Windows theme, it is also known as the theme style. The value can be drawn from the style, or from a trigger attached to the style.
  5. The inherited value. As explained below, an attached dependency property can be used to associate a value with an instance that does not itself define the property. Attached properties can also be configured to 'inherit' values from parent elements in the XAML tree, as FontSize does.
  6. The default value specified in the dependency property metadata.

After the base value is selected, it is replaced or modified by animations, if any target the property being read. The animated value is coerced and validated, if such callbacks have been defined, before finally being returned.

Attached properties

Normally, a property (like any class or structure variable) associates values with instances of the type that defines the property. An attached property, by contrast, can associate values with instances that do not themselves define it. This allows a value that is assigned to a parent element to be used by its children. In this example, assigning a font in the StackPanel element changes the font in child elements that derive from TextElement, despite the fact that StackPanel provides no font property:

<StackPanel TextElement.FontFamily="Arial">
  ...

Conversely, attached properties allow values assigned to children to be used by a parent. In this case, specifying a dock position in the Label changes the way the containing Dock lays out its children, even though Label knows nothing of the Dock implementation:

<DockPanel>
  <Label Name="LblMain" DockPanel.Dock="Top">FRAME 1/A</Label>
  ...

In the XAML, the property attribute is prefixed with the class that defines the attached property. This class is called the attached property provider. In general, an attached property allows unrelated elements to say something about themselves to the provider.

The provider implements the attached property by creating a dependency property with the RegisterAttached function:

class tJob: DependencyObject {
  ...

class tMgrRun {
  public static readonly DependencyProperty sWgtRunProperty
    = DependencyProperty.RegisterAttached(
        "WgtRun",
        typeof(float),
        typeof(tMgrRun),
        new PropertyMetadata(1.0F)
      );
  ...

It then defines public static accessor functions that begin with Get or Set, and end with the property name:

public static float GetWgtRun(tJob aqJob) {
  return (float)aqJob.GetValue(sWgtRunProperty);
}

public static void SetWgtRun(tJob aqJob, float aWgt) {
  aqJob.SetValue(sWgtRunProperty, aWgt);
}

Both functions accept an instance of the class to which the property will be attached. Because this class is used to call GetValue and SetValue, it must derive from DependencyObject. The Set function is called when the XAML is processed, and it can be used to set the property procedurally as well. A property wrapper can also be added to allow use as a regular dependency property.

A dependency property must be implemented as an attached property if it is to 'inherit' values from parent elements in the XAML tree. When this is done, a default should be specified to ensure that a value is always available.

Routed events

The class that defines and invokes an event is called the event sender or event source, while the classes that implement handlers are event listeners. The Button class, for instance, sends click notifications, while a Window that handles click events is said to listen. Raising an event entails checking an event instance for a handler, and invoking that handler if it is found. By convention, the method that does this has a name that begins with On, followed by the event name. Thus the Button class monitors mouse activity, and if it determines that it has been clicked, it invokes its own OnClick method. In an ordinary .NET event, this method confirms that the handler delegate is non-null, and then invokes it:

.NET events

WPF supports a similar mechanism called a routed event. Much like the dependency property, this is a .NET event backed by a WPF class, in this case RoutedEvent:

class tSwitch: Control {
  public static readonly RoutedEvent sFlipEvent
    = EventManager.RegisterRoutedEvent(
        "Flip",
        RoutingStrategy.Bubble,
        typeof(RoutedEventHandler),
        typeof(tSwitch)
      );

  public event RoutedEventHandler Flip {
    add { AddHandler(sFlipEvent, value); }
    remove { RemoveHandler(sFlipEvent, value); }
  }

  void OnFlip(tArgsEventFlip aqArgs) {
    RaiseEvent(new RoutedEventArgs(sFlipEvent, this));
  }
  ...

The event is raised by calling RaiseEvent, which invokes the handlers, if any, that were earlier added with AddHandler. Routed events are more flexible than ordinary .NET events. They allow event signals to pass up or down through XAML hierarchies, so that handlers can be assigned in other parts of the object tree.

The RoutedEvent instance is expected to be public and static, its name must end with Event, and it should be readonly as well. It is instantiated with the RegisterRoutedEvent function in EventManager. This function accepts the RoutingStrategy enumeration, which defines how the event travels through a XAML hierarchy:

  • Bubbling causes the event to travel up, starting with the source element;
  • Tunneling causes the event to travel down, starting with the XAML root element, and ending with the source;
  • Direct causes the event to be handled only at the source, much like a traditional .NET event.

The .NET event is called an event wrapper, and it allows handlers to be added with operator+= and removed with operator-=, like other events. The adder passes the RoutedEvent to AddHandler, while the remover passes it to RemoveHandler. These methods are implemented by UIElement and ContentElement, so the event sender must derive from one of these classes.

RoutedEventArgs is the base class for all routed event data, an instance of which is passed to RaiseEvent. The class offers these properties:

  • RoutedEvent stores the event instance;
  • OriginalSource stores the visual tree element where the event originated. This is the instance that called RaiseEvent;
  • Source stores the logical tree element where the event originated;
  • Handled indicates whether the event has been handled.

The Bubbling or Tunneling process continues after Handled has been set to true, although, in most cases, no more elements are checked for handlers. One of the AddHandler overloads includes a handledEventsToo parameter that allows a handler to receive events that have already been handled elsewhere.

UI input is generally signaled with Bubbling events. In WPF, some Bubbling events are preceded by preview events, which are Tunneling events with names that begin with Preview. The same RoutedEventArgs instance is passed to both handlers, so these allow parent controls to modify event data or handle events preemptively.

In XAML, a handler is attached by specifying the event name as an attribute, with the handler name as its value. Like the event sender, the event listener must be a subclass of UIElement or ContentElement:

<Canvas MouseLeftButtonDown="eTrack">
  ...

In the handler, the control that generates the event is passed as the sender:

private void eTrack(object aqSend,
  MouseButtonEventArgs aqArgs) {
  ...

Attached events

Any routed event can also be used as an attached event, which associates a handler with an element other than the one that defined the event. In XAML, this allows a single handler in a parent element to process events from a number of descendents:

<StackPanel CheckBox.Click="eUpd">
  <CheckBox x:Name="cCkNone" Content="None"/>
  <CheckBox x:Name="cCkRnd" Content="Random"/>
  <CheckBox x:Name="cCkLast" Content="Last"/>
</StackPanel>

This is useful when implementing controls, since the events generated by various subsidiary elements can be handled in one place.

The event sender maintains a strong reference to the listener, so handlers must be removed from the event if the listener is to be garbage-collected. Alternatively, handlers can be added with weak references by calling the AddHandler method in the generic WeakEventManager class.

Class handlers

Assigning a handler with XAML (or in procedural code, with the event wrapper) creates an instance listener. The assignment relates the sender to a handler in a specific instance.

A listener that derives from DependencyObject can also act as a class listener, which automatically handles the event whenever it crosses an instance of the class within the XAML hierarchy. No handler is assigned in the XAML; in fact, it can be configured only in procedural code. The class listener defines a static class handler:

class tPanSwitch {
  static protected void csUpd_Stats(object aqSend,
    RoutedEventArgs aqArgs) {
    ...

which is added to the event by calling RegisterClassHandler within the listener's static constructor:

static tPanSwitch() {
  EventManager.RegisterClassHandler(
    typeof(tSwitch),
    tSwitch.sFlipEvent,
    new RoutedEventHandler(csUpd_Stats)
  );
  ...

Class handlers can be added to any routed event. They are invoked before any instance handlers that might also be defined. Different handlers can be defined by different classes within the class hierarchy, and, when this is done, handlers in the most-derived classes are invoked first. Because the class handler is static, it cannot manipulate the instance except through the event parameters.

Additionally, many UIElement subclasses offer virtual methods that have names beginning with OnPreview or On. These methods are invoked by a class handler in the base class, so overriding them allows class handling to be performed without registering a new handler. If this is done, the base implementation should be called even if the override marks the event handled.

The Application instance

When Visual Studio creates a WPF project, it defines a subclass of Application in App.xaml.cs:

namespace nMain {
  public partial class tApp: Application {
  }
}

which it references in App.xaml:

<Application x:Class="nMain.tApp"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:nMain"
  StartupUri="WinMain.xaml">

  <Application.Resources>
  </Application.Resources>
</Application>

The main window is specified with the StartupUri property. When it builds the project, Visual Studio creates the App.g.cs file within one of the obj subfolders. This generated file extends the partial subclass by adding an InitializeComponent method, which assigns properties in the XAML to the application instance:

public void InitializeComponent() {
  StartupUri = new System.Uri(
    "WinMain.xaml",
    System.UriKind.Relative
  );
}

It also defines the Main method, which serves as an entry point for the application:

[STAThread]
public static void Main() {
  nMain.tApp oqApp = new nMain.tApp();
  oqApp.InitializeComponent();
  oqApp.Run();
}

To prevent Main from being defined automatically, right-click the App.xaml file in the Solution Explorer, select Properties, and change Build Action from ApplicationDefinition to Page.

Command-line arguments can be obtained by defining a custom Main that accepts a string array:

...
public static void Main(string[] aqArgs) {
  ...

or by calling the Environment.GetCommandLineArgs function within System.

Application class

The base Application class includes properties and methods such as:

static Current
Returns the application instance.
Run
Run(Window)
Starts the application, then returns its exit code when it shuts down. If a Window instance is specified, that window will be displayed. It is also possible to specify a window by setting the StartupUri property to reference a XAML file.
Shutdown
Shutdown(Int32)
Stops the application and returns the specified exit code, or zero.
ShutdownMode
Ordinarily, the application ends when the last window is closed, or when Shutdown is explicitly invoked. Setting this property to OnMainWindowClose causes it to end when the main window is closed. Setting it to OnExplicitShutdown causes it to run until Shutdown is called.
MainWindow
Gets or sets the main window.
Windows
Returns references to all Window instances in the application.
Properties
Returns a dictionary storing arbitrary key/value pairs, for use throughout the application.
Resources
Gets or sets a ResourceDictionary instance that stores application resources. The same resources can be fetched with FindResource or TryFindResource.

Layout

Distance units

Windows uses its DPI setting to determine the number of physical pixels in one logical inch. The DPI was once fixed at 96, but now it can be changed to a percentage of that value. Because the physical pixel size is constant for a given display, increasing the DPI causes the logical inch to increase in physical size.

The default unit in XAML is the device-independent pixel (DIP), equal to 1/96 of a logical inch. This unit is explicitly specified by appending px to the attribute value. Logical inches or centimeters are specified with in or cm. Points are specified with pt, equal to 1/72 of a logical inch. The Windows DPI may or may not match the actual pixel density of the display, but when it does, logical inches are found to equal physical inches, point sizes on the display match sizes in print, and DIPs correspond directly to physical pixels.

Logical inches are converted to physical pixel measurements by multiplying with the DPI:

physical pixels = logical inches · DPI

Therefore, to convert device-independent pixels to physical pixels:

physical pixels = DIPs / 96 · DPI

To convert points to physical pixels:

physical pixels = points / 72 · DPI

Sizing controls

In general, when they are not stretched to fill their parent, WPF controls size themselves to fit their own content. Sizes can be explicitly set with Width and Height, but, if the content changes size unexpectedly, it could be truncated. Width and Height have default values of Double.NaN, which can be represented in XAML as NaN, though Auto is preferred. These properties do not generally return the actual size within the layout.

Sizes can be constrained with MinWidth, MaxWidth, MinHeight, and MaxHeight. The Max sizes have default values of Double.PositiveInfinity, which can be represented in XAML with Infinity.

DesiredSize is a read-only property that is used by containers during layout. RenderSize is also read-only, and it gives the actual size in the layout, as do ActualWidth and ActualHeight. These properties cannot be relied upon outside of the LayoutUpdated event, however.

Margins and padding

Margin and Padding are Thickness properties that control spacing outside and inside a control. The Thickness structure stores one, two, or four doubles. In XAML, multiple values are specified with a comma-delimited list:

Padding="8,8,24,8"

Unlike CSS, the first value specifies the left measurement, or the side measurements, if two values are given.

Margins can be negative, but padding values cannot.

Control visibility

The Visibility property determines whether the control is visible, and whether it participates in the layout. The associated Visibility enumeration has three values:
  • Visible causes the control to be rendered and laid-out as usual;
  • Hidden prevents the control from being rendered, but does not remove it from the layout;
  • Collapsed prevents the control from being rendered and removes it from the layout.

Control alignment

The HorizontalAlignment and VerticalAlignment properties affect a control's position within the space allotted by its parent, as well as its dimensions, if they have not been explicitly set. Each property accepts an enumeration with the same name. In both cases, the default value, Stretch, causes the control to fill the space. In HorizontalAlignment, Left, Center, and Right size the contained control to its own content, and align it within the space. In VerticalAlignment, Bottom, Center, and Top do the same.

The HorizontalContentAlignment and VerticalContentAlignment properties affect the the control's content in a like manner. They use the same enumerations.

The FlowDirection property facilitates the use of right-to-left scripts, like Arabic. When set to RightToLeft, the Left and Right alignment values reverse their effect. Many other properties and controls also reverse the horizontal axis when RightToLeft is set. FlowDirection does not itself change the direction of text.

Content overflow

A control's ClipToBounds property determines whether content is truncated at the control's boundaries, or whether it is allowed to overflow. This property is found in all UIElement subclasses, but a number of panels clip regardless of its value. Canvas does allow clipping to be disabled, and other controls can be made to disable clipping by placing a Canvas between them and their children in the object hierarchy.

Scrolling content

Any control can be made to scroll by placing it inside a ScrollViewer instance. The VerticalScrollBarVisibility and HorizontalScrollBarVisibility properties in this class accept members of the ScrollBarVisibility enumeration:

  • Visible causes the scrollbar to be displayed at all times;
  • Auto causes the scrollbar to appear only when it is needed;
  • Hidden prevents the scrollbar from appearing, but it allows scrolling with the keyboard;
  • Disabled hides the scrollbar and disables keyboard scrolling.

When a given scrollbar is Disabled, the viewer's content receives as much space along that axis as the viewer itself has. When any other value is set, the content receives as much space as it requests.

Some controls use ScrollViewer instances internally, and their scrollbars can be configured by assigning VerticalScrollBarVisibility and HorizontalScrollBarVisibility as attached properties.

Scaling content

A control can be scaled by placing it inside a Viewbox instance. Unlike ScaleTransform, which scales the control to some fraction of its original size, Viewbox scales it to fit the box. The control can be made to fit in different ways, according to the Stretch property, which accepts a Stretch enumeration:

  • None leaves the content unscaled;
  • Fill scales the content to fit the width and height of the box. No whitespace will be produced, but the content's aspect ratio may change;
  • Uniform sets the content to the largest size that fits entirely inside the box without changing the content's aspect ratio. The box will not be completely filled unless the aspect ratios match;
  • UniformToFill scales the content to fill the entire box without changing the content aspect ratio. If the aspect ratios do not match, some content will overflow the box.

Scaling is also affected by the StretchDirection property, which accepts a StretchDirection enumeration. This determines whether the content can be scaled UpOnly, DownOnly, or in Both directions, as is the default.

Transforms

WPF controls provide two Transform properties that can be used to alter control orientation, shape, and position. LayoutTransform is implemented in FrameworkElement, and its transform is applied before the layout. RenderTransform is implemented in UIElement, and its transform applies after, so that the layout reflects the original, untransformed size and position. Both properties can be set and used together.

Though all FrameworkElement instances provide the transform properties, controls that display non-WPF content may not implement them completely.

Rotation transform

Specific operations are represented by subclasses of Transform. The RotateTransform class rotates the control by Angle degrees:

<Button x:Name="BtnLaunch" Click="BtnLaunch_Click">
  <Button.RenderTransform>
    <RotateTransform Angle="-90"/>
  </Button.RenderTransform>
  Launch
</Button>

The CenterX and CenterY properties in this transform set the DIP position about which the rotation occurs. This point can also be set with the RenderTransformOrigin property in the control. Unlike CenterX and CenterY, this property accepts a Point bearing unit coordinates, which range from [0.0, 0.0] in the top-left corner to [1.0, 1.0] in the bottom-right. This is useful when the control size is unknown.

The text inside the control can be rotated by replacing it with a TextBlock:

<Button x:Name="BtnLaunch" Click="BtnLaunch_Click">
  <TextBlock RenderTransformOrigin="0.5,0.5">
    <TextBlock.RenderTransform>
      <RotateTransform Angle="-90" CenterX="0" CenterY="0"/>
    </TextBlock.RenderTransform>
    Launch
  </TextBlock>
</Button>

The CenterX, CenterY, and RenderTransformOrigin properties are ignored in the layout transform.

Scale transform

The ScaleTransform class provides ScaleX and ScaleY properties that store scaling factors for each dimension. A value of 1.0 leaves the size unchanged. The class also implements the CenterX and CenterY properties, along with support for RenderTransformOrigin. As with RotateTransform, these specify the single, unmoving point about which the transform occurs. If a corner is selected, that point will keep its original position, and the sides opposite the corner will move away to resize the control. If an inside point is selected, all four sides will move away, but the distance they move will vary according to their original distance from the point. As a result, the inside point will maintain its unit coordinate position after the transform.

If ScaleTransform is applied as a LayoutTransform, and if the target element uses the Stretch alignment, the transform will have no effect unless it enlarges the control more than Stretch has done.

Controls typically clip their content, and this clipping is performed during the layout. Therefore, an element that is enlarged as a RenderTransform may not be clipped as expected, and one that is reduced may show evidence of clipping even when the reduced size fits entirely within the parent.

Other transforms

Controls can also be transformed with classes such as:

  • TranslateTransform, which moves the control horizontally and vertically, but only when applied as a RenderTransform;
  • SkewTransform, which changes rectangles into parallelograms;
  • MatrixTransform, which applies an arbitrary matrix transformation.

A set of transforms can be applied by combining them within a TransformGroup instance:

<Button.LayoutTransform>
  <TransformGroup>
    <ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
    <RotateTransform Angle="-90"/>
  </TransformGroup>
</Button.LayoutTransform>

Panels

Like other controls, the Window class provides a Content property that can be set only once. To display more than one control, a Panel must be added to the window. WPF offers a range of Panel subclasses that arrange controls in different ways.

Canvas

The Canvas panel supports absolute positioning. Controls are placed by assigning values to the Left, Right, Bottom, and Top attached properties defined in Canvas, with each defining the distance from the specified side to the nearest side of the control. Setting an offset also causes the control to be anchored to that side, so that the distance is maintained if the Canvas size changes. Only one side in each left/right or bottom/top pair can be anchored; setting both does not cause the control to be resized with the Canvas:

<Canvas>
  <Button x:Name="BtnStop"
    Width="80" Height="40"
    Canvas.Right="10" Canvas.Top="10"/>
</Canvas>

If the control has a margin on an anchored side, it will be added to the distance. Margins on non-anchored sides will be ignored. Neither HorizontalAlignment nor VerticalAlignment function within a Canvas.

Canvas also implements a ZIndex attached property that controls the Z-order of overlapping controls.

Stack panel

The StackPanel displays its children in a horizontal or vertical sequence, as determined by the Orientation property. Each control in a vertical StackPanel receives all the horizontal space in the panel, plus as much vertical space as the control itself requires.

VirtualizingStackPanel is a similar control that derives from VirtualizingPanel. When data binding is used, this panel creates controls as their cells become visible, and releases them as they go out of view. This allows large datasets to be displayed without compromising performance. The ListBox control uses this panel internally.

Wrap panel

When its Orientation is set to Horizontal, the WrapPanel displays its children in rows that wrap to new lines when they reach the right side of the panel. Additional rows are added until all children have been placed:

Wrap panel

Setting Orientation to Vertical produces columns that wrap when they reach the bottom.

By default, the controls in a horizontal WrapPanel determine their own widths, while each control's height is set to match the tallest control in its row. Setting ItemWidth or ItemHeight in the panel overrides these sizes, clipping the controls if necessary.

Dock panel

The DockPanel class implements a Dock attached property that allows child controls to attach themselves to the Left, Right, Bottom, or Top side of the panel. The controls follow the sides of the panel when it is resized:

<DockPanel>
  <StackPanel DockPanel.Dock="Top" Height="40"
    Orientation="Horizontal">
    ...
  </StackPanel>
  <TextBlock DockPanel.Dock="Bottom" Height="40">
    ...
  </TextBlock>
  <StackPanel Width="40" DockPanel.Dock="Left">
    ...
  </StackPanel>
  <Canvas>
    ...
  </Canvas>
</DockPanel>

In this case, the child controls have HorizontalAlignment or VerticalAlignment set to Stretch by default, so they expand to cover as much of the panel sides as possible:

Dock panel

The last child fills the remaining space, unless the panel's LastChildFill property is set to false, allowing the control to be docked like other children.

Grid

The Grid panel places controls within the cells of a table. Rows and columns are defined by adding RowDefinition and ColumnDefinition instances to the RowDefinitions and ColumnDefinitions property elements. Child controls are associated with cells by setting the Row and Column attached properties:

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition/>
    <RowDefinition/>
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition/>
    <ColumnDefinition/>
  </Grid.ColumnDefinitions>

  <Button x:Name="BtnUp"
    Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"/>
  <Button x:Name="BtnLeftDown"
    Grid.Row="1" Grid.Column="0"/>
  <Button x:Name="BtnRightDown"
    Grid.Row="1" Grid.Column="1"/>
</Grid>

Rows and columns can be created in the XAML designer by clicking the left and right margins. If no rows are explicity defined, or no columns, one will be created automatically at run time. Controls are made to span multiple cells by setting the ColumnSpan and RowSpan attached properties.

Cells can be left empty, or multiple children can be added to the same cell, with later additions appearing above others in the Z-order. The Grid class offers no way to style cells, but a cell can be colored by placing a Rectangle instance at the bottom of its Z-order, and then setting the rectangle's Fill. A border can be defined by setting the Stroke property within the Rectangle, or by placing the Rectangle inside a Border.

By default, each row or column receives an equal portion of the grid's height or width. This can be changed by setting the Height or Width properties in the row and column definitions. In XAML:

  • Assigning a number produces pixel or absolute sizing, which sets the row or column to a specific DIP size;
  • Assigning Auto produces auto sizing, which sizes the row or column to the largest control it contains;
  • Assigning '*' or a number followed by '*' produces star or proportional sizing. When a single row or column is set to '*', it consumes all space not allocated to absolute or auto-sized elements. When multiple rows or columns are set this way, the available space is split evenly between them. When '*' is prefixed by a number, the allocation is weighted by that number.

In procedural code, the size is set by assigning a GridLength instance to Height or Width. One of the GridLength constructors accepts a GridUnitType enumeration that selects absolute, auto, or proportional sizing.

Grid splitter

Adding a GridSplitter allows cells to be resized at run time. By default, a splitter is displayed in only one cell, yet its effect applies to the entire row or column, so it should always be made to span the grid.

A horizontal splitter can share a row with other controls if its HorizontalAlignment is set to Stretch, and if its VerticalAlignment is set to Bottom or Top, with this also determining the side upon which the splitter operates. When this is done, the splitter's Height must be explicitly set to make it visible, and this height will cover other controls in the row. Similar rules apply to a vertical splitter that shares a column with grid content.

To prevent controls from being covered, it is preferable to give each splitter its own row or column. Both HorizontalAlignment and VerticalAlignment should be set to Stretch, leaving the splitter thickness to be determined by the row or column thickness:

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="40"/>
      <RowDefinition Height="5"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="40"/>
      <ColumnDefinition Width="5"/>
      <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    ...
    <GridSplitter
      Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Stretch"/>
    <GridSplitter
      Grid.Row="0" Grid.Column="1" Grid.RowSpan="3"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Stretch"/>
  </Grid>

Multiple rows or columns can be made to change size together by associating them with a size group. First, the IsSharedSizeScope property in the containing Grid must be set to true, then the SharedSizeGroup property in the row and column definitions must be set to a common string that names the group. All rows or columns in the group will start with the same size, equal to the largest absolute or Auto size in the group; proportional sizes will be ignored. The elements will continue to share the same size, even when one of them is resized. A group can contain both rows and columns, and it can even contain and update elements from different grids.

Controls

Window

Most Window events are associated with virtual handlers in the base class, so they are handled by overriding those functions.

Windows can be displayed with Show or ShowDialog. Show makes the window non-modal, and returns immediately. ShowDialog makes the window modal, and blocks until it is closed. Then it returns the value assigned to DialogResult, or false if the window was closed with Close. Assigning a non-null value to DialogResult closes the window automatically. Assigning to DialogResult after displaying with Show causes an exception to be thrown.

The Activated event is raised when a window is focused, and Deactivated is raised when it loses the focus. The Activate method focuses the window programmatically.

OnClosing is raised when an attempt is made to close the window. Setting Cancel within the CancelEventArgs parameter to true keeps the window open.

Assigning a window to the Owner property specifies that window as the parent of this one. When the parent is minimized or closed, its children are minimized or closed as well.

User input

Keyboard input

Key presses in UIElement controls are signaled with the KeyDown and KeyUp events, along with Preview variants of the same. Handlers receive a KeyEventArgs parameter that describes the input. This class contains many properties, among which:

  • Key returns a Key enumeration that identifies alphanumeric, editing, navigation, function, OEM, and other key types. Number keys in the number pad are distinguished from numbers in the regular keyboard, but navigation keys in the number pad are not. Some keys (like Alt, or Esc, when Ctrl is down) have special functions in Windows, and these are always identified as Key.System. When this happens, the specific key press can be read from the SystemKey property;
  • IsUp and IsDown describe the key's current state. IsToggled tells whether keys like Caps Lock and Num Lock are enabled. The KeyStates property returns the KeyStates enumeration, which combines flags that represent these three states;
  • IsRepeat tells whether the event was produced by key repeat;
  • KeyboardDevice returns a KeyboardDevice instance. This class provides functions like IsKeyUp, IsKeyDown, and IsKeyToggled, which give the state for any key, not just the one producing the event. It also provides the Modifiers property, which tells whether any or all of the Shift, Ctrl, Alt, or Windows keys are down.

A KeyboardDevice instance can also be obtained from the PrimaryDevice property in the Keyboard class, within System.Windows.Input. This can be used at any time, even outside of keyboard events.

By default, any UIElement can receive the keyboard focus, whether by being clicked, or tabbed-into. This can be disabled by setting the Focusable property to false. The IsTabStop attached property in KeyboardNavigation allows a control to be removed from the tab order, but if Focusable is true, it will still be possible to focus it with a click. KeyboardNavigation also allows the tab order to be defined by setting TabIndex.

Mouse input

Mouse movements in UIElement controls are signaled with MouseEnter, MouseMove, and MouseLeave events. Handlers for these events receive a MouseEventArgs parameter, within which:

  • LeftButton, MiddleButton, and RightButton indicate whether the mouse buttons are up or down;
  • GetPosition returns the mouse position relative to a control, or relative to the screen, if null is passed to the function;
  • Timestamp gives the time when the event occurred.

The static Mouse class within System.Windows.Input allows mouse data to be queried outside of mouse events, though the position cannot be obtained this way during drag-and-drop operations.

The MouseDown and MouseUp events signal button input. Handlers for these events receive a MouseButtonEventArgs parameter, which adds the following properties to MouseEventArgs:

  • ChangedButton identifies the button that generated the event;
  • ClickCount gives the number of clicks since the last time the double-click timeout expired.

UIElement also provides MouseLeftButtonDown, MouseLeftButtonUp, MouseRightButtonDown, and MouseRightButtonUp events that duplicate some of the MouseDown and MouseUp functionality. Control instances automatically raise MouseDoubleClick events when the left mouse button is double-clicked.

Mouse wheel scrolling is signaled with the MouseWheel event. Handlers for this event receive a MouseWheelEventArgs parameter. This class adds a Delta property to MouseEventArgs that is positive when the wheel is scrolled up, and negative when it is scrolled down. The magnitude can vary with different devices.

UIElement supports drag-and-drop operations that transfer DataObject instances within or between apps. This is implemented with events like DragEnter, DragOver, DragLeave, QueryContinueDrag, and Drop. A UIElement instance can also capture the mouse with its CaptureMouse function. This causes the UIElement to receive all mouse events, even those raised when the pointer is outside its boundaries, until ReleaseMouseCapture is called.

Preview variants are offered for most mouse events. Note that control areas with null Background, Fill, or Stroke properties do not generate mouse events, though Transparent areas do. Also, Canvas can display controls outside of its own Width and Height (both of which default to zero) but it does not generate mouse events outside this area.

Commands

A command is a user action packaged within some instance that implements ICommand. In this interface:

  • CanExecute indicates whether the action is available;
  • CanExecuteChanged signals a change to the CanExecute value. This event should be raised by the code that causes the change;
  • Execute performs the action.

A control that triggers the command is called a command source, and this will typically implement ICommandSource. Within this interface:

  • Command specifies the command to be executed;
  • As explained below, if the command is a routed command, event routing will begin at the element specified by CommandTarget, or at the focused control, if CommandTarget is null. If it is not a routed command, this property will be ignored;
  • CommandParameter stores an arbitrary object to be passed to CanExecute and Execute.

The command is somewhat like an event listener, while the source takes the place of the sender. Setting Command in a source control like Button or MenuItem causes it to call Execute when it is used:

<Button Command="{x:Static local:tWinMain.sCmdSort}"/>

The control also adds a handler to CanExecuteChanged that enables or disables it. This allows a set of controls that produce the same action to be updated automatically.

Input gestures include key presses and mouse clicks, in combination with optional modifier keys. Like controls, these gestures can serve as command sources. A keyboard gesture is associated with a command by adding a KeyBinding to the listener's InputBindings collection. The Key and Modifiers can be specified separately:

<Window.InputBindings>
  <KeyBinding Key="S" Modifiers="Shift+Control"
    Command="{x:Static local:tWinMain.sCmdSort}"/>
</Window.InputBindings>

or together, as a Gesture:

<Window.InputBindings>
  <KeyBinding Gesture="Shift+Ctrl+S"
    Command="{x:Static local:tWinMain.sCmdSort}"/>
</Window.InputBindings>

Mouse gestures are specified with a MouseBinding.

Routed commands

The RoutedCommand class implements ICommand, but instead of performing the command itself, its CanExecute and Execute methods raise routed events that are handled somewhere within the XAML hierarchy. Routing will begin at the element specified by CommandTarget, or at the focused control, if CommandTarget is null.

A routed command is defined somewhat like a dependency property:

public static readonly RoutedCommand sCmdRest
  = new RoutedCommand();

It is assigned to a command source much like any other command:

<Button Content="Restore"
  Command="{x:Static local:tWinMain.sCmdRest}"
  CommandParameter="REST:AQ380"/>

Handlers are associated with command events by adding a CommandBinding to the listening control:

<Window.CommandBindings>
  <CommandBinding
    Command="{x:Static local:tWinMain.sCmdRest}"
    CanExecute="eCanExecuteRest" Executed="eExecutedRest"/>
</Window.CommandBindings>

The CanExecute handler has a void return type, so it signals command availability by setting the CanExecute property within its CanExecuteRoutedEventArgs parameter.

Common routed commands are implemented as static RoutedUICommand instances within the ApplicationCommands, NavigationCommand, MediaCommands, ComponentCommands, and EditingCommands classes. RoutedUICommand adds a Text property to RoutedCommand that describes the command, the value of which is automatically localized to match the system language. Many predefined commands have default key bindings that are created automatically when the command is assigned:

<Button Command="ApplicationCommands.Find"/>

An unwanted key binding can be removed by setting its command to NotACommand:

<Window.InputBindings>
  <KeyBinding Key="F" Modifiers="Control"
    Command="NotACommand"/>
</Window.InputBindings>

Miscellanea

Splash screens

WPF has built-in support for static splash screen images. To display one, add an image to the project, then change its Build Action to SplashScreen.

This technique will not work if a custom Main function has been defined. In this case, it is necessary to display the screen programmatically. Add the image as before, but leave its Build Action set to Resource. Then create and Show a SplashScreen instance that references the image at the start of the Main function:

[STAThread]
public static void Main() {
  SplashScreen oqSplash = new SplashScreen("Splash.png");
  oqSplash.Show(true);
  ...

Application settings

.NET and Visual Studio provide support for application settings, which store configuration data outside the executable.

Settings are defined with the Visual Studio Settings Designer, which is displayed by selecting the Settings tab in the project properties, or by double-clicking the Settings.settings file within the Properties folder in the Solution Explorer.

Four properties define each setting:

  • The Name will be used to reference the property in the code. It can be set to any string that is valid for a C# identifier;
  • The Type can be any that is serializable to XML, or that is associated with a TypeConverter subclass implementing ToString and FromString;
  • Scope can be set to Application, which produces a read-only property, or to User, which produces a read/write property;
  • Value specifies the default value for the setting.

At design time, the Settings Designer stores settings configuration data in:

Properties\Settings.settings

within the solution folder. The data can also be found in App.config, which merges the data from multiple settings files, if more are created. The designer also creates a Settings class within:

Properties\Settings.Designer.cs

This class is defined in the default.Properties namespace, with default being the application's default namespace. It provides a type-safe property for each setting in the designer. The application's Settings instance can be obtained from the static Default property:

var oqSet = Settings.Default;
string oqCdReg = oqSet.CdRegDef;
int oCtZone = oqSet.CtZoneMax;

After modifying a user setting, the change must be explicitly stored with the Save method:

oqSet.CtZoneMax = 10;
oqSet.Save();

At run time, settings data is stored in either of two XML files. Application properties are stored in assemblyName.exe.config, which must be deployed in the folder containing the executable. This file also stores default values for user properties. It can be replaced at deployment time to change settings without rebuilding.

User properties are stored in user.config. This file is created in the user's AppData folder when Save is first called. The full path contains the 'company name' (actually the name of the namespace containing the application class), the assembly name, and the assembly version, among other things. As a result, if the assembly version changes, a new user settings file is created. To retain values stored by an earlier version, it is necessary to invoke Update on the Settings class when the new version starts. This can be done by creating a CkUpgrade setting with a default value of true. When this setting matches the default, Upgrade is called, and the setting is changed to false:

void WinMain_Initialized(object sqSend, EventArgs aqArgs) {
  var oqSet = Settings.Default;
  if (oqSet.CkUpgrade) {
    oqSet.Upgrade();
    oqSet.CkUpgrade = false;
  }
  ...

It is possible to add multiple settings files to the same project. However, all settings are merged to the same assemblyName.exe.config and user.config files when this is done.

Sources

WPF 4.5 Unleashed
Adam Nathan
2014, Pearson Education / Sams

MSDN
Attached properties overview , Application Settings Overview , Commanding Overview , Dependency properties overview , Manage application settings , How to: Create a RoutedCommand , Routed events overview , Using Settings in C#: Using Alternate Sets of Settings , XAML Services
Retrieved June 2017 - March 2019

Stack Overflow
How do you keep user.config settings across different assembly versions in .net?
Retrieved March 2019