One of the first issues I ran into when I was learning WPF was that the Canvas didn't size to its contents. I was attempting to render a graph and I wanted to stick a Canvas in a ScrollViewer and have the scroll area grow as more content was added outside the current bounds of the Canvas. While searching for a solution I noticed on various forums that several other developers were looking for a solution to this problem. At the time I didn't know enough about writing custom layout containers to write my own Canvas and I soon found myself diverted.
In addition to restoring the WPF Bottom/Right properties that are missing from Silverlight's Canvas the DynamicCanvas adds CenterLeft, CenterTop, CenterRight, and CenterBottom properties. In WPF these properties are convenient but not strictly necessary as you can use a multi-binding and a converter to center your objects declaratively. However Silverlight 2 does not support multi-bindings which makes it rather more difficult to ensure that objects will remain centered if their dimensions change after they've been positioned.
The following XAML...
<Window x:Class="testDynCanvas.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Unfold.Windows.Controls"
Title="Window1" Height="300" Width="300">
<Grid>
<local:DynamicCanvas SizeWidthToContent="True" Background="Pink">
<Rectangle
Width="30"
Height="50"
local:DynamicCanvas.CenterBottom="50"
local:DynamicCanvas.CenterRight="30"
Fill="Purple"
/>
</local:DynamicCanvas>
</Grid>
</Window>
The DynamicCanvas is a good example of a custom layout control if you're trying to learn how to write one. It also takes advantage of one of the most exciting attributes of Siverlight 2: the ability to cross-compile with WPF. Silverlight 2 has now matured to the point where some simple controls can be compiled in Silverlight with little or no modification.
It's important to remember that you shouldn't use DynamicCanvas (or Canvas) to do layout. You should only use Canvas for tasks where coordinate-based plotting is appropriate such as writing a custom series for Silverlight Charts (something I'll be blogging about after our next release). This control is not an MS supported control and you should use it at your own risk. That said if I get positive feedback on it I may try and get it into the Silverlight Toolkit.
Without further ado here are the two files to add to your WPF/Silverlight project...
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Unfold.Windows.Controls
{
/// <summary>
/// A canvas that can size to its contents.
/// </summary>
public class DynamicCanvas : Panel
{
#region public bool SizeWidthToContent
/// <summary>
/// Gets or sets a value indicating whether the dynamic canvas should
/// size its width to its content.
/// </summary>
public bool SizeWidthToContent
{
get { return (bool)GetValue(SizeWidthToContentProperty); }
set { SetValue(SizeWidthToContentProperty, value); }
}
/// <summary>
/// Identifies the SizeWidthToContent dependency property.
/// </summary>
public static readonly DependencyProperty SizeWidthToContentProperty =
DependencyProperty.Register(
"SizeWidthToContent",
typeof(bool),
typeof(DynamicCanvas),
new PropertyMetadata(false, OnSizeWidthToContentPropertyChanged));
/// <summary>
/// SizeWidthToContentProperty property changed handler.
/// </summary>
/// <param name="d">DynamicCanvas that changed its SizeWidthToContent.</param>
/// <param name="e">Event arguments.</param>
private static void OnSizeWidthToContentPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DynamicCanvas source = (DynamicCanvas)d;
bool oldValue = (bool)e.OldValue;
bool newValue = (bool)e.NewValue;
source.OnSizeWidthToContentPropertyChanged(oldValue, newValue);
}
/// <summary>
/// SizeWidthToContentProperty property changed handler.
/// </summary>
/// <param name="oldValue">Old value.</param>
/// <param name="newValue">New value.</param>
protected virtual void OnSizeWidthToContentPropertyChanged(bool oldValue, bool newValue)
{
Invalidate();
}
#endregion public bool SizeWidthToContent
#region public bool SizeHeightToContent
/// <summary>
/// Gets or sets a value indicating whether the canvas should size its
/// height to its content.
/// </summary>
public bool SizeHeightToContent
{
get { return (bool)GetValue(SizeHeightToContentProperty); }
set { SetValue(SizeHeightToContentProperty, value); }
}
/// <summary>
/// Identifies the SizeHeightToContent dependency property.
/// </summary>
public static readonly DependencyProperty SizeHeightToContentProperty =
DependencyProperty.Register(
"SizeHeightToContent",
typeof(bool),
typeof(DynamicCanvas),
new PropertyMetadata(false, OnSizeHeightToContentPropertyChanged));
/// <summary>
/// SizeHeightToContentProperty property changed handler.
/// </summary>
/// <param name="d">DynamicCanvas that changed its SizeHeightToContent.</param>
/// <param name="e">Event arguments.</param>
private static void OnSizeHeightToContentPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DynamicCanvas source = (DynamicCanvas)d;
bool oldValue = (bool)e.OldValue;
bool newValue = (bool)e.NewValue;
source.OnSizeHeightToContentPropertyChanged(oldValue, newValue);
}
/// <summary>
/// SizeHeightToContentProperty property changed handler.
/// </summary>
/// <param name="oldValue">Old value.</param>
/// <param name="newValue">New value.</param>
protected virtual void OnSizeHeightToContentPropertyChanged(bool oldValue, bool newValue)
{
Invalidate();
}
#endregion public bool SizeHeightToContent
#region public attached double Bottom
/// <summary>
/// Gets the value of the Bottom attached property for a specified UIElement.
/// </summary>
/// <param name="element">The UIElement from which the property value is read.</param>
/// <returns>The Bottom property value for the UIElement.</returns>
public static double GetBottom(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(BottomProperty);
}
/// <summary>
/// Sets the value of the Bottom attached property to a specified UIElement.
/// </summary>
/// <param name="element">The UIElement to which the attached property is written.</param>
/// <param name="value">The needed Bottom value.</param>
public static void SetBottom(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(BottomProperty, value);
}
/// <summary>
/// Identifies the Bottom dependency property.
/// </summary>
public static readonly DependencyProperty BottomProperty =
DependencyProperty.RegisterAttached(
"Bottom",
typeof(double),
typeof(DynamicCanvas),
new PropertyMetadata(double.NaN, OnBottomPropertyChanged));
/// <summary>
/// BottomProperty property changed handler.
/// </summary>
/// <param name="dependencyObject">UIElement that changed its Bottom.</param>
/// <param name="eventArgs">Event arguments.</param>
public static void OnBottomPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
UIElement source = dependencyObject as UIElement;
if (source == null)
{
throw new ArgumentException("dependencyObject");
}
DynamicCanvas parent = VisualTreeHelper.GetParent(source) as DynamicCanvas;
if (parent != null)
{
parent.Invalidate();
}
}
#endregion public attached double Bottom
#region public attached double Left
/// <summary>
/// Gets the value of the Left attached property for a specified UIElement.
/// </summary>
/// <param name="element">The UIElement from which the property value is read.</param>
/// <returns>The Left property value for the UIElement.</returns>
public static double GetLeft(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(LeftProperty);
}
/// <summary>
/// Sets the value of the Left attached property to a specified UIElement.
/// </summary>
/// <param name="element">The UIElement to which the attached property is written.</param>
/// <param name="value">The needed Left value.</param>
public static void SetLeft(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(LeftProperty, value);
}
/// <summary>
/// Identifies the Left dependency property.
/// </summary>
public static readonly DependencyProperty LeftProperty =
DependencyProperty.RegisterAttached(
"Left",
typeof(double),
typeof(DynamicCanvas),
new PropertyMetadata(double.NaN, OnLeftPropertyChanged));
/// <summary>
/// LeftProperty property changed handler.
/// </summary>
/// <param name="dependencyObject">UIElement that changed its Left.</param>
/// <param name="eventArgs">Event arguments.</param>
public static void OnLeftPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
UIElement source = dependencyObject as UIElement;
if (source == null)
{
throw new ArgumentException("dependencyObject");
}
DynamicCanvas parent = VisualTreeHelper.GetParent(source) as DynamicCanvas;
if (parent != null)
{
parent.Invalidate();
}
}
#endregion public attached double Left
#region public attached double Right
/// <summary>
/// Gets the value of the Right attached property for a specified UIElement.
/// </summary>
/// <param name="element">The UIElement from which the property value is read.</param>
/// <returns>The Right property value for the UIElement.</returns>
public static double GetRight(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(RightProperty);
}
/// <summary>
/// Sets the value of the Right attached property to a specified UIElement.
/// </summary>
/// <param name="element">The UIElement to which the attached property is written.</param>
/// <param name="value">The needed Right value.</param>
public static void SetRight(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(RightProperty, value);
}
/// <summary>
/// Identifies the Right dependency property.
/// </summary>
public static readonly DependencyProperty RightProperty =
DependencyProperty.RegisterAttached(
"Right",
typeof(double),
typeof(DynamicCanvas),
new PropertyMetadata(double.NaN, OnRightPropertyChanged));
/// <summary>
/// RightProperty property changed handler.
/// </summary>
/// <param name="dependencyObject">UIElement that changed its Right.</param>
/// <param name="eventArgs">Event arguments.</param>
public static void OnRightPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
UIElement source = dependencyObject as UIElement;
if (source == null)
{
throw new ArgumentException("dependencyObject");
}
DynamicCanvas parent = VisualTreeHelper.GetParent(source) as DynamicCanvas;
if (parent != null)
{
parent.Invalidate();
}
}
#endregion public attached double Right
#region public attached double Top
/// <summary>
/// Gets the value of the Top attached property for a specified UIElement.
/// </summary>
/// <param name="element">The UIElement from which the property value is read.</param>
/// <returns>The Top property value for the UIElement.</returns>
public static double GetTop(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(TopProperty);
}
/// <summary>
/// Sets the value of the Top attached property to a specified UIElement.
/// </summary>
/// <param name="element">The UIElement to which the attached property is written.</param>
/// <param name="value">The needed Top value.</param>
public static void SetTop(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(TopProperty, value);
}
/// <summary>
/// Identifies the Top dependency property.
/// </summary>
public static readonly DependencyProperty TopProperty =
DependencyProperty.RegisterAttached(
"Top",
typeof(double),
typeof(DynamicCanvas),
new PropertyMetadata(double.NaN, OnTopPropertyChanged));
/// <summary>
/// TopProperty property changed handler.
/// </summary>
/// <param name="dependencyObject">UIElement that changed its Top.</param>
/// <param name="eventArgs">Event arguments.</param>
public static void OnTopPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
UIElement source = dependencyObject as UIElement;
if (source == null)
{
throw new ArgumentException("dependencyObject");
}
DynamicCanvas parent = VisualTreeHelper.GetParent(source) as DynamicCanvas;
if (parent != null)
{
parent.Invalidate();
}
}
#endregion public attached double Top
#region public attached double CenterBottom
/// <summary>
/// Gets the value of the CenterBottom attached property for a specified UIElement.
/// </summary>
/// <param name="element">The UIElement from which the property value is read.</param>
/// <returns>The CenterBottom property value for the UIElement.</returns>
public static double GetCenterBottom(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(CenterBottomProperty);
}
/// <summary>
/// Sets the value of the CenterBottom attached property to a specified UIElement.
/// </summary>
/// <param name="element">The UIElement to which the attached property is written.</param>
/// <param name="value">The needed CenterBottom value.</param>
public static void SetCenterBottom(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(CenterBottomProperty, value);
}
/// <summary>
/// Identifies the CenterBottom dependency property.
/// </summary>
public static readonly DependencyProperty CenterBottomProperty =
DependencyProperty.RegisterAttached(
"CenterBottom",
typeof(double),
typeof(DynamicCanvas),
new PropertyMetadata(double.NaN, OnCenterBottomPropertyChanged));
/// <summary>
/// CenterBottomProperty property changed handler.
/// </summary>
/// <param name="dependencyObject">UIElement that changed its CenterBottom.</param>
/// <param name="eventArgs">Event arguments.</param>
public static void OnCenterBottomPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
UIElement source = dependencyObject as UIElement;
if (source == null)
{
throw new ArgumentException("dependencyObject");
}
DynamicCanvas parent = VisualTreeHelper.GetParent(source) as DynamicCanvas;
if (parent != null)
{
parent.Invalidate();
}
}
#endregion public attached double CenterBottom
#region public attached double CenterLeft
/// <summary>
/// Gets the value of the CenterLeft attached property for a specified UIElement.
/// </summary>
/// <param name="element">The UIElement from which the property value is read.</param>
/// <returns>The CenterLeft property value for the UIElement.</returns>
public static double GetCenterLeft(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(CenterLeftProperty);
}
/// <summary>
/// Sets the value of the CenterLeft attached property to a specified UIElement.
/// </summary>
/// <param name="element">The UIElement to which the attached property is written.</param>
/// <param name="value">The needed CenterLeft value.</param>
public static void SetCenterLeft(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(CenterLeftProperty, value);
}
/// <summary>
/// Identifies the CenterLeft dependency property.
/// </summary>
public static readonly DependencyProperty CenterLeftProperty =
DependencyProperty.RegisterAttached(
"CenterLeft",
typeof(double),
typeof(DynamicCanvas),
new PropertyMetadata(double.NaN, OnCenterLeftPropertyChanged));
/// <summary>
/// CenterLeftProperty property changed handler.
/// </summary>
/// <param name="dependencyObject">UIElement that changed its CenterLeft.</param>
/// <param name="eventArgs">Event arguments.</param>
public static void OnCenterLeftPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
UIElement source = dependencyObject as UIElement;
if (source == null)
{
throw new ArgumentException("dependencyObject");
}
DynamicCanvas parent = VisualTreeHelper.GetParent(source) as DynamicCanvas;
if (parent != null)
{
parent.Invalidate();
}
}
#endregion public attached double CenterLeft
#region public attached double CenterRight
/// <summary>
/// Gets the value of the CenterRight attached property for a specified UIElement.
/// </summary>
/// <param name="element">The UIElement from which the property value is read.</param>
/// <returns>The CenterRight property value for the UIElement.</returns>
public static double GetCenterRight(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(CenterRightProperty);
}
/// <summary>
/// Sets the value of the CenterRight attached property to a specified UIElement.
/// </summary>
/// <param name="element">The UIElement to which the attached property is written.</param>
/// <param name="value">The needed CenterRight value.</param>
public static void SetCenterRight(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(CenterRightProperty, value);
}
/// <summary>
/// Identifies the CenterRight dependency property.
/// </summary>
public static readonly DependencyProperty CenterRightProperty =
DependencyProperty.RegisterAttached(
"CenterRight",
typeof(double),
typeof(DynamicCanvas),
new PropertyMetadata(double.NaN, OnCenterRightPropertyChanged));
/// <summary>
/// CenterRightProperty property changed handler.
/// </summary>
/// <param name="dependencyObject">UIElement that changed its CenterRight.</param>
/// <param name="eventArgs">Event arguments.</param>
public static void OnCenterRightPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
UIElement source = dependencyObject as UIElement;
if (source == null)
{
throw new ArgumentException("dependencyObject");
}
DynamicCanvas parent = VisualTreeHelper.GetParent(source) as DynamicCanvas;
if (parent != null)
{
parent.Invalidate();
}
}
#endregion public attached double CenterRight
#region public attached double CenterTop
/// <summary>
/// Gets the value of the CenterTop attached property for a specified UIElement.
/// </summary>
/// <param name="element">The UIElement from which the property value is read.</param>
/// <returns>The CenterTop property value for the UIElement.</returns>
public static double GetCenterTop(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(CenterTopProperty);
}
/// <summary>
/// Sets the value of the CenterTop attached property to a specified UIElement.
/// </summary>
/// <param name="element">The UIElement to which the attached property is written.</param>
/// <param name="value">The needed CenterTop value.</param>
public static void SetCenterTop(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(CenterTopProperty, value);
}
/// <summary>
/// Identifies the CenterTop dependency property.
/// </summary>
public static readonly DependencyProperty CenterTopProperty =
DependencyProperty.RegisterAttached(
"CenterTop",
typeof(double),
typeof(DynamicCanvas),
new PropertyMetadata(double.NaN, OnCenterTopPropertyChanged));
/// <summary>
/// CenterTopProperty property changed handler.
/// </summary>
/// <param name="dependencyObject">UIElement that changed its CenterTop.</param>
/// <param name="eventArgs">Event arguments.</param>
public static void OnCenterTopPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
UIElement source = dependencyObject as UIElement;
if (source == null)
{
throw new ArgumentException("dependencyObject");
}
DynamicCanvas parent = VisualTreeHelper.GetParent(source) as DynamicCanvas;
if (parent != null)
{
parent.Invalidate();
}
}
#endregion public attached double CenterTop
/// <summary>
/// Invalidates the position of child elements.
/// </summary>
private void Invalidate()
{
if (this.SizeHeightToContent || this.SizeWidthToContent)
{
this.InvalidateMeasure();
}
else
{
this.InvalidateArrange();
}
}
/// <summary>
/// Measures all the children and returns their size.
/// </summary>
/// <param name="constraint">The available size.</param>
/// <returns>The desired size.</returns>
protected override Size MeasureOverride(Size constraint)
{
Size availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
if (SizeHeightToContent || SizeWidthToContent)
{
foreach (UIElement child in Children)
{
child.Measure(availableSize);
}
double maxWidth = 0;
if (SizeWidthToContent)
{
maxWidth =
Children
.Cast<UIElement>()
.Where(child => !double.IsNaN(GetLeft(child)))
.Select(child => GetLeft(child) + child.DesiredSize.Width)
.Concat(
Children
.Cast<UIElement>()
.Where(child => !double.IsNaN(GetCenterLeft(child)))
.Select(child => GetCenterLeft(child) + (child.DesiredSize.Width / 2))).MaxOrNullable() ?? 0.0;
double maxRightOffset =
Children
.Cast<UIElement>()
.Where(child => !double.IsNaN(GetRight(child)))
.Select(child => (maxWidth - GetRight(child)) - child.DesiredSize.Width)
.Concat(
Children
.Cast<UIElement>()
.Where(child => !double.IsNaN(GetCenterRight(child)))
.Select(child => (maxWidth - GetCenterRight(child)) - (child.DesiredSize.Width / 2))).MinOrNullable() ?? 0.0;
if (maxRightOffset < 0.0)
{
maxWidth += Math.Abs(maxRightOffset);
}
}
double maxHeight = 0;
if (SizeHeightToContent)
{
maxHeight =
Children
.Cast<UIElement>()
.Where(child => !double.IsNaN(GetTop(child)))
.Select(child => GetTop(child) + child.DesiredSize.Height)
.Concat(
Children
.Cast<UIElement>()
.Where(child => !double.IsNaN(GetCenterTop(child)))
.Select(child => GetCenterTop(child) + (child.DesiredSize.Height / 2))).MaxOrNullable() ?? 0.0;
double maxBottomOffset =
Children
.Cast<UIElement>()
.Where(child => !double.IsNaN(GetBottom(child)))
.Select(child => (maxHeight - GetBottom(child)) - child.DesiredSize.Height)
.Concat(
Children
.Cast<UIElement>()
.Where(child => !double.IsNaN(GetCenterBottom(child)))
.Select(child => (maxHeight - GetCenterBottom(child)) - (child.DesiredSize.Height / 2))).MinOrNullable() ?? 0.0;
if (maxBottomOffset < 0.0)
{
maxHeight += Math.Abs(maxBottomOffset);
}
}
return new Size(maxWidth, maxHeight);
}
else
{
foreach (UIElement element in Children)
{
if (element != null)
{
element.Measure(availableSize);
}
}
return Size.Empty;
}
}
/// <summary>
/// Arranges all children in the correct position.
/// </summary>
/// <param name="arrangeSize">The size to arrange element's within.
/// </param>
/// <returns>The size that element's were arranged in.</returns>
protected override Size ArrangeOverride(Size arrangeSize)
{
foreach (UIElement element in base.Children)
{
if (element == null)
{
continue;
}
double x = 0.0;
double y = 0.0;
double left = GetLeft(element);
double centerLeft = GetCenterLeft(element);
double halfWidth = (element.DesiredSize.Width / 2.0);
if (!double.IsNaN(left))
{
x = left;
}
else if (!double.IsNaN(centerLeft))
{
x = centerLeft - halfWidth;
}
else
{
double right = GetRight(element);
if (!double.IsNaN(right))
{
x = (arrangeSize.Width - element.DesiredSize.Width) - right;
}
else
{
double centerRight = GetCenterRight(element);
if (!double.IsNaN(centerRight))
{
x = (arrangeSize.Width - halfWidth) - centerRight;
}
}
}
double top = GetTop(element);
double centerTop = GetCenterTop(element);
double halfHeight = (element.DesiredSize.Height / 2.0);
if (!double.IsNaN(top))
{
y = top;
}
else if (!double.IsNaN(centerTop))
{
y = centerTop - halfHeight;
}
else
{
double bottom = GetBottom(element);
if (!double.IsNaN(bottom))
{
y = (arrangeSize.Height - element.DesiredSize.Height) - bottom;
}
else
{
double centerBottom = GetCenterBottom(element);
if (!double.IsNaN(centerBottom))
{
y = (arrangeSize.Height - halfHeight) - centerBottom;
}
}
}
element.Arrange(new Rect(new Point(x, y), element.DesiredSize));
}
return arrangeSize;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Unfold.Windows.Controls
{
public static class EnumerableFunctions
{
/// <summary>
/// Returns the maximum value or null if sequence is empty.
/// </summary>
/// <param name="that">The sequence to retrieve the maximum value from.
/// </param>
/// <returns>The maximum value or null.</returns>
public static T? MaxOrNullable<T>(this IEnumerable<T> that)
where T : struct, IComparable
{
if (!that.Any())
{
return null;
}
return that.Max();
}
/// <summary>
/// Returns the minimum value or null if sequence is empty.
/// </summary>
/// <param name="that">The sequence to retrieve the minimum value from.
/// </param>
/// <returns>The minimum value or null.</returns>
public static T? MinOrNullable<T>(this IEnumerable<T> that)
where T : struct, IComparable
{
if (!that.Any())
{
return null;
}
return that.Min();
}
}
}