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.