Wednesday, November 15, 2023

Swapping two rows in a data table

File this under "I can't believe this was so hard".

I have a requirement to allow the user to swap two rows in an Infragistics XamDataGrid. The grid is bound to a data table so I have to swap two data rows in the data table. This is generic code so I cannot make any assumptions about the data. I thought this would be trivial until I had to do this on a grid with child layouts. The child rows were getting disconnected from the parent.

There is no Move method on a data row or data table. You have to delete and insert. But as soon as you remove a row from a data table it changes to an unattached state and all its item data is lost. The same happens to all of its child records. So before removing the row, you have to store off all its data and all its child data.

It gets complicated. Here's my solution.

        Protected Function MoveDataRow(OldRow As DataRow, Delta As Integer) As DataRow
            Dim dt As DataTable= OldRow.Table
            Dim newRow As DataRow = dt.NewRow()
            Dim oldRowState As DataRowState = OldRow.RowState
            Dim ChildRows As New Dictionary(Of DataRelation, List(Of cRow))()
            Dim InsertAt As Integer = dt.Rows.IndexOf(OldRow) + Delta

            For Each dataRelation As DataRelation In dt.DataSet.Relations
                If dataRelation.ParentTable.Equals(dt) Then
                    ChildRows.Add(dataRelation, New List(Of cRow))
                    For Each childDataRow As DataRow In OldRow.GetChildRows(dataRelation)
                        ChildRows(dataRelation).Add(New cRow(childDataRow))
                        dataRelation.ChildTable.Rows.Remove(childDataRow)
                    Next
                End If
            Next

            newRow.ItemArray = OldRow.ItemArray
            dt.Rows.Remove(OldRow)
            dt.Rows.InsertAt(newRow, InsertAt)

            newRow.AcceptChanges()
            Select Case oldRowState
                Case DataRowState.Added : newRow.SetAdded()
                Case DataRowState.Modified : newRow.SetModified()
            End Select

            For Each kvp As KeyValuePair(Of DataRelation, List(Of cRow)) In ChildRows
                For Each row As cRow In kvp.Value
                    Dim newChildRow As DataRow = kvp.Key.ChildTable.NewRow()
                    newChildRow.ItemArray = row.ItemArray
                    kvp.Key.ChildTable.Rows.Add(newChildRow)
                    newChildRow.AcceptChanges()
                    Select Case row.RowState
                        Case DataRowState.Added : newChildRow.SetAdded()
                        Case DataRowState.Modified : newChildRow.SetModified()
                    End Select
                Next
            Next

            Return newRow
        End Function

        Private Class cRow
            Public Sub New(row As DataRow)
                Me.ItemArray = row.ItemArray
                Me.RowState = row.RowState
            End Sub
            Public ItemArray As Object()
            Public RowState As DataRowState
        End Class

You probably notice some repetition of code and that this only handles one level of child row. It's ripe for recursion. Please, feel free to do that for me 😀

If you have a better algorithm, please let me know. Surely it doesn't have to be this complicated!

Wednesday, October 25, 2023

Accessing CodeProject's AI Explorer from WPF

I got an email from CodeProject about their AI Explorer that can be hosted locally.

https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way

It took about an hour to install it and not all the modules are usable and useful. Nevertheless, the modules that are useful are interesting. It comes with a well designed dashboard that lets you interact with the AI services.

Once you have installed this and installed the scene classification module you will see Scene Classification as it looks below. 


These services can also be called from your code. I wanted to write a simple WPF application that could classify images as you can see above. My application won't be as pretty, but it will be mine.

Start a new C#, .Net Core Visual Studio project and call it SceneClassifier.

It will let the user select an image, and when they do it will display the image and call the SceneClassifier AI service. It will then display the scene classification and confidence.

Here's the XAML for MainWindow.xaml. There's nothing clever here. It's all in the code.

<Window x:Class="SceneClassifier.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:SceneClassifier"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        SizeToContent="WidthAndHeight"
        Title="Scene Classifier" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="SelectCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource SelectCommand}" Executed="Select_Executed"/>
    </Window.CommandBindings>
    <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"/>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
 
        <Button Grid.Row="0" Grid.Column="0" Content="Select Image" Command="{StaticResource SelectCommand}"/>
        <TextBlock Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding FileName}"/>
 
        <Image Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4" Height="200" Source="{Binding FileName}"/>
 
        <TextBlock Grid.Row="2" Grid.Column="0" Text="Classification:"/>
        <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Classification}"/>
 
        <TextBlock Grid.Row="2" Grid.Column="2" Text="Confidence"/>
        <TextBlock Grid.Row="2" Grid.Column="3" Text="{Binding Confidence}"/>
       
        <TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="4" Text="{Binding Status}"/>
    </Grid>
</Window>

-------------------------------------------------------------------------------------

The code uses an HttpClient to access the REST service provided by CodeProject's AI server and deserializes the result into an object. The we pull the classification and confidence out of the object. Yes, I know the architecture sucks - this isn't a post about code purity :-)

using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Win32;
using System;
using System.Net.Http;
using System.Windows;
using System.Windows.Input;
using System.Net.Http.Json;
 
namespace SceneClassifier;
public class cResult
{
    public bool success { get; set; }
    public string label { get; set; }
    public float confidence { get; set; }
    public string command { get; set; }
    public string moduleId { get; set; }
    public string executionProvider { get; set; }
    public bool canUseGPU { get; set; }
    public int inferenceMs { get; set; }
    public int processMs { get; set; }
    public int analysisRoundTripMs { get; set; }
}
 
[ObservableObject]
public partial class MainWindow : Window
{
    [ObservableProperty] String _FileName = "";
    [ObservableProperty] String _Status = "";
    [ObservableProperty] string _Classification;
    [ObservableProperty] String _Confidence;
 
    HttpClient httpClient = new HttpClient();
 
    public MainWindow()
    {
        InitializeComponent();
    }
 
    private void Select_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        OpenFileDialog openFileDialog = new OpenFileDialog();
        openFileDialog.Filter = "png Files|*.png";
        openFileDialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
        if (openFileDialog.ShowDialog().Value)
        {
            FileName = openFileDialog.FileName;
            ClassifyScene(FileName);
        }
    }
 
    private async void ClassifyScene(string FileName)
    {
        Byte[] imageBytes = System.IO.File.ReadAllBytes(FileName);
        ByteArrayContent content = new ByteArrayContent(imageBytes);
        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/png");
 
        MultipartFormDataContent form = new MultipartFormDataContent();
        form.Add(content, "image", "image.png");
 
        HttpResponseMessage response = await httpClient.PostAsync("http://localhost:32168/v1/vision/scene", form);
        if (response.IsSuccessStatusCode)
        {
            cResult result = await response.Content.ReadFromJsonAsync<cResult>();
            Classification = result.label;
            Confidence = Math.Round(result.confidence * 100).ToString() + "%";
            Status = "Success";
        }
        else
        {
            string Error = await response.Content.ReadAsStringAsync();
            Status = $"Error: {response.StatusCode}:{Error}";
        }
    }
}