Monday, December 27, 2021

TextBox won't stretch vertically

The WPF TextBox can be multi line, accept enter, display wrapping, etc. All you need is something like...

<TextBox VerticalAlignment="Stretch" TextWrapping="WrapWithOverflow" Text="{Binding Message}" ScrollViewer.CanContentScroll="True" AcceptsReturn="True"/>

Sometimes this doesn't work and the textbox won't expand.


This may be because you have set a height. If you set a height, VerticalAlignment=Stretch is ignored. In my case, I had a default style that included a height. One way to fix this is to assign an empty style...

<
TextBox VerticalAlignment="Stretch" TextWrapping="WrapWithOverflow" Text="{Binding Message}" ScrollViewer.CanContentScroll="True" AcceptsReturn="True">
    <TextBox.Style>
        <Style TargetType="TextBox"/>
    <TextBox.Style>
</TextBox>



Saturday, December 25, 2021

Are MIT Computer Scientists any good?

I've been watching a lot of lecture videos on ocw.mit.edu while working out lately and decided to watch the course called introduction to computer science. At one point the lecturer is writing a function to search a dictionary and return the value for a key, or zero if the key does not exist. He wrote code to fetch the value from the dictionary and, if the key did not exist, trap the exception and return zero. My gut feeling is that this is an appalling solution and I would make one of my programmers rewrite it if they tried to use it.

Apart from the fact that best practice says exceptions should only be raised for unforeseen problems, the cost of creating the exception is much larger than the cost of checking for the key. But one of my colleagues argued that it might be more efficient if the key was very likely to be in the dictionary. How likely, I wondered?

So I wrote a test in C# (MIT uses Python 2.x!) to see how much slower it is to raise an exception rather than test for the existence of a key. Start a C# console project in Visual Studio and call it DictTestVsException. Here's the code. 

DictTestTwice checks for the existence of a key 2000 times (it fails half the time) and fetches the value when the key exists. This means it executes 3000 key searches. Note that ContainsKey is faster than Key.Contains.

DictTestOnce executes the FirstOrDefault function 2000 times to either return the value or zero. If a key search takes the same time as FirstOrDefault, this should be the fastest algorithm. It's highly contrived and only works because the first entry in the list contains our default value.

DictException assumes the existence of a key and traps the exception when it does not exist. It does 2000 key lookups and raises 1000 exceptions. 

Each of these functions returns the number of ticks it took. The main program prints the results on the console.

using System;
using System.Collections.Generic;
using System.Linq;
 
namespace DictTestVsException
{
    class Program
    {
        static long DictTestTwice()
        {
            Dictionary<intintd = new Dictionary<intint>();
            for (int i = 0; i < 1000; i++) d.Add(i, i * 2);
            DateTime sw = DateTime.Now;
            int v;
 
            for (int i = 0; i < 2000; i++)
                if (d.ContainsKey(i))
                    v = d[i];
                else
                    v = 0;
 
            return (DateTime.Now - sw).Ticks;
        }
 
        static long DictTestOnce()
        {
            Dictionary<intintd = new Dictionary<intint>();
            for (int i = 0; i < 1000; i++) d.Add(i, i * 2);
            DateTime sw = DateTime.Now;
            int v;
 
            for (int i = 0; i < 2000; i++)
                v = d.FirstOrDefault(k => k.Key == i).Value;
 
            return (DateTime.Now - sw).Ticks;
        }
 
        static long DictException()
        {
            Dictionary<intintd = new Dictionary<intint>();
            for (int i = 0; i < 1000; i++) d.Add(i, i * 2);
            DateTime sw = DateTime.Now;
            int v;
 
            for (int i = 0; i < 2000; i++)
            {
                try
                {
                    v = d[i];
                }
                catch (Exception ex)
                {
                    v = 0;
                }
            }
 
            return (DateTime.Now - sw).Ticks;
        }
 
        static void Main(string[] args)
        {
            long dt2 = DictTestTwice();
            long dt1 = DictTestOnce();
            long de = DictException();
 
            Console.WriteLine("DictTestTwice = " + dt2.ToString() + " ticks");
            Console.WriteLine("DictTestOnce = " + dt1.ToString() + " ticks");
            Console.WriteLine("DictException = " + de.ToString() + " ticks");
            Console.WriteLine("Exception is " + (de / dt2).ToString() + " times slower than test twice");
            Console.ReadKey();
        }
    }
}

Your results may vary, but on my computer the cost of creating the exception is between 600 and 800 times greater than the cost of testing the dictionary. This is a pretty strong argument for testing the dictionary rather than trapping the exception. 



Interestingly, DictTestOnce is slower than DictTestTwice. This implies that a key lookup is using a binary search and FirstOrDefault is using a scan. The search, in this scenario, is about five times faster. This makes sense when you realize that FirstOrDefault is a method of IEnumerable which is not guaranteed to be sorted.



Friday, December 17, 2021

Improve performance when mass selecting rows on XamDataGrid

I wrote a custom control to allow mass selection of rows in a XamDataGrid which runs slowly when there are more than a few hundred rows. It seems the act of setting a record's IsSelected flag takes about one millisecond which adds up for grids with a lot of rows.

        private void One_Click(object sender, RoutedEventArgs e)
        {
            foreach (Record r in TheGrid.Records)
                r.IsSelected = true;
        }

The XamDataGrid has a SelectedItems property which has a Records property. If I clear this property the grid updates instantly. The property has an AddRange method that takes an array of Records. AddRange tends to be very fast - Hmmmm.

This code could be cleaner, but it's very fast. I would prefer to populate a List of Records and use the ToArray method.

        private void Range_Click(object sender, RoutedEventArgs e)
        {
            DataRecord[] records = new DataRecord[TheGrid.Records.Count];
            TheGrid.Records.CopyTo(records, 0);
            TheGrid.SelectedItems.Records.AddRange(records);
        }

Here is some XAML and code that demonstrates the performance difference. Please forgive the non MVVM solution, it's Friday afternoon and I have stuff to do before I go home for the weekend.

<Window x:Class="SelectAll.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        xmlns:local="clr-namespace:SelectAll"
        mc:Ignorable="d"
        SizeToContent="WidthAndHeight"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
 
        <igDP:XamDataGrid Grid.Row="0" Grid.Column="0" Name="TheGrid" Height="400" DataSource="{Binding Items}">
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowEdit="False" CellClickAction="SelectRecord"/>
            </igDP:XamDataGrid.FieldSettings>
        </igDP:XamDataGrid>
        <StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal">
            <Button Content="Select one at a time" Click="One_Click"/>
            <Button Content="Select range" Click="Range_Click"/>
            <Button Content="Clear" Click="Clear_Click"/>
        </StackPanel>
    </Grid>
</Window>

using Infragistics.Windows.DataPresenter;
using System.Collections.Generic;
using System.Windows;
 
namespace SelectAll
{
    public class cItem
    {
        public int ID { get; set; }
        public string Code { get; set; }
        public string Description { get; set; }
    }
    public partial class MainWindow : Window
    {
        public List<cItem> Items { get; set; }
        public MainWindow()
        {
            PopulateItems();
            InitializeComponent();
        }
 
        private void PopulateItems()
        {
            Items = new List<cItem>();
            for (int i = 1; i < 5000; i++)
                Items.Add(new cItem() { ID = i, Code = "Code " + i, Description = "Description " + i });
        }
        private void One_Click(object sender, RoutedEventArgs e)
        {
            foreach (Record r in TheGrid.Records)
                r.IsSelected = true;
        }
 
        private void Range_Click(object sender, RoutedEventArgs e)
        {
            DataRecord[] records = new DataRecord[TheGrid.Records.Count];
            TheGrid.Records.CopyTo(records, 0);
            TheGrid.SelectedItems.Records.AddRange(records);
        }
 
        private void Clear_Click(object sender, RoutedEventArgs e)
        {
            TheGrid.SelectedItems.Records.Clear();
        }
    }
}

  


On my computer, selecting all the rows one at a time takes 14 seconds. Selecting them using AddRange is too quick to measure.




Friday, November 5, 2021

Converting Converters

I have a WPF page containing controls that have to be enabled and disabled under complex conditions. Rather that creating an IsXXXXEnabled property for each one and try to remember to update them all as conditions change, I want to embed that functionality in XAML to separate it from the business logic in the code behind.

I came up with a ComputeConverter. This idea is so obvious that I'm sure I'm like the 10,000th person to think of it. The idea is to create a MultiValue converter that takes a parameter, substitutes the values, and uses DataTable.Compute to evaluate it, returning the result. Similar to String.Format, I use {n} as place holders. So to multiply two numbers the parameter would be "{}{0} * {1}". The starting {} tells the XAML parser that {0} is not a markup extension. String literals need to be delimited so to concatenate two strings with a hyphen the parameter would be "'{0}' + '-' + '{1}'". This parameter does not need a {} because it does not start with a { character.

In this example I have a legal document and an access mode. A witness is only needed for Affidavits and Wills and can only be entered when the access mode is Editable. At all other times, the witness name text box must be disabled.

Start a new C# Visual Studio WPF Framework project and call it ComputeConverter.

Add a new class called Converters and replace the code with this. All it does is try to substitute each value into the parameter and then evaluate the result. There is no code to look for missing or excess values, etc. If the developer gets the parameter wrong, weird things will happen.

using System;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
 
namespace ComputeConverter
{
    public class ComputeConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            String Expression = System.Convert.ToString(parameter);
            DataTable DT = new DataTable();
 
            for (int i = 0; i < values.Count(); i++)
                Expression = Expression.Replace("{" + i.ToString() + "}", values[i].ToString());
            return DT.Compute(Expression, "");
        }
 
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Now we can see the converter being used. Change the contents of MainWindow.xml to look like this. You can see we have defined a style to highlight the IsEnabled state of the text box. We have two combo boxes to allow selection of document type and access mode. The fun part is the parameter of the compute converter.

ConverterParameter="('{0}'='Affidavit' OR '{0}'='Will') AND '{1}'='Editable'">

This will return True if the first parameter is Affidavit or Will AND the second parameter is Editable. This is extremely powerful, although it's a bit slow.

<Window x:Class="ComputeConverter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ComputeConverter"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Compute Converter" Height="450" Width="800">
    <Window.Resources>
        <local:ComputeConverter x:Key="ComputeConverter"/>
        <Style TargetType="TextBox">
            <Setter Property="Background" Value="LightBlue"/>
            <Style.Triggers>
                <Trigger Property="IsEnabled" Value="True">
                    <Setter Property="Background" Value="White"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
 
        <TextBlock Grid.Row="0" Grid.Column="0" Text="Document Type"/>
        <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding DocumentTypes}" SelectedItem="{Binding SelectedDocumentType}"/>
 
        <TextBlock Grid.Row="1" Grid.Column="0" Text="Access Mode"/>
        <ComboBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding AccessModes}" SelectedItem="{Binding SelectedAccessMode}"/>
 
        <TextBlock Grid.Row="3" Grid.Column="0" Text="Witness Name"/>
        <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding WitnessName}">
            <TextBox.IsEnabled>
                <MultiBinding Converter="{StaticResource ComputeConverter}" ConverterParameter="('{0}'='Affidavit' OR '{0}'='Will') AND '{1}'='Editable'">
                    <Binding Path="SelectedDocumentType"/>
                    <Binding Path="SelectedAccessMode"/>
                </MultiBinding>
            </TextBox.IsEnabled>
        </TextBox>
    </Grid>
</Window>

Time for MainWindow.xaml.cs, which contains no surprises.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
 
namespace ComputeConverter
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public List<String> DocumentTypes { get; set; } = new List<string> { "Affidavit","Contract","Deed","Lien","Will" };
        public List<String> AccessModes { get; set; } = new List<string> { "Locked", "Security", "Sealed", "Editable" };
 
        private String _SelectedAccessMode = "Locked";
        public String SelectedAccessMode
        {
            get { return _SelectedAccessMode; }
            set { SetProperty(ref _SelectedAccessMode, value); }
        }
 
        private String _SelectedDocumentType = "Deed";
        public String SelectedDocumentType
        {
            get { return _SelectedDocumentType; }
            set { SetProperty(ref _SelectedDocumentType, value); }
        }
 
        public String WitnessName { get;set;}
 
        public MainWindow()
        {
            InitializeComponent();
        }
 
        public event PropertyChangedEventHandler PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

The result is that the text box is only enabled under the required conditions