Thursday, February 25, 2016

Right-align textblock in datagrid

Let's suppose you have a datagrid with a column that you want right-aligned. Sounds easy right? Your code behind might look something like this...

Imports System.Collections.ObjectModel
Class MainWindow
    Public Property Amounts As New ObservableCollection(Of Decimal) From {0}
End Class

and your XAML might look like this...

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <DataGrid ItemsSource="{Binding Amounts}">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Amounts" Binding="{Binding}">
                <DataGridTextColumn.ElementStyle>
                    <Style TargetType="TextBlock">
                        <Setter Property="HorizontalAlignment" Value="Right"></Setter>
                    </Style>
                </DataGridTextColumn.ElementStyle>
            </DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>
</Window>

The results look like this, which is what we expected.


Now let's put a background on the text block by adding another setter to the element style.

<Setter Property="Background" Value="Yellow"></Setter>


Well that isn't right :-(
The problem is that the text block is only as wide as it needs to be. What if we explicitly set the width to 50px by adding yet another setter?

<Setter Property="Width" Value="50"></Setter>


Wow - so now our cell is yellow but the right-align is broken. If you look carefully you can see the text block is right aligned within the cell but the contents of the text block are left aligned. The HorizontalAlignment property controls how the text block is aligned, not how it's contents are aligned. This is consistent, but a little non-intuitive. But there isn't a HorizontalContentAlignment property on a text block :-(

It turns out there are two simple ways to fix this.
Option 1 is to set the background of the cell. If no background is set for the textblock it is transparent and the cell background bleeds through.

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <DataGrid ItemsSource="{Binding Amounts}">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Amounts" Binding="{Binding}">
                <DataGridTextColumn.ElementStyle>
                    <Style TargetType="TextBlock">
                        <Setter Property="HorizontalAlignment" Value="Right"></Setter>
                    </Style>
                </DataGridTextColumn.ElementStyle>
                <DataGridTextColumn.CellStyle>
                    <Style TargetType="DataGridCell">
                        <Setter Property="Background" Value="Yellow"></Setter>
                    </Style>
                </DataGridTextColumn.CellStyle>
            </DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>
</Window>

Option 2 is to set the horizontal alignment to Stretch and set the TextAlignment property to Right. So for text blocks the TextAlignment property replaces the HorizontalContentAlignment property.

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <DataGrid ItemsSource="{Binding Amounts}">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Amounts" Binding="{Binding}">
                <DataGridTextColumn.ElementStyle>
                    <Style TargetType="TextBlock">
                        <Setter Property="HorizontalAlignment" Value="Stretch"></Setter>
                        <Setter Property="TextAlignment" Value="Right"></Setter>
                        <Setter Property="Background" Value="Yellow"></Setter>
                    </Style>
                </DataGridTextColumn.ElementStyle>
            </DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>
</Window>



Wednesday, February 3, 2016

Adding x:Name to a user control causes a compilation error

I was developing a new user control and realized I wanted to bind to a dependency property. The XAML looks like this.

<UserControl x:Class="FileInputBox.FileInputBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <DockPanel>
        <Button x:Name="theButton" DockPanel.Dock="Right" Click="theButton_Click">Browse...</Button>
        <TextBox x:Name="theTextBox" MinWidth="{Binding ActualWidth, ElementName=theButton}" Margin="0,0,2,0"/>
    </DockPanel>
</UserControl>

I want to bind the text property of the TextBox to a dependency property of the UserControl called FileName. So I gave the UserControl a name and bound to it so the XAML looks like this.

<UserControl x:Class="FileInputBox.FileInputBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Name="root">
    <DockPanel>
        <Button x:Name="theButton" DockPanel.Dock="Right" Click="theButton_Click">Browse...</Button>
        <TextBox x:Name="theTextBox" MinWidth="{Binding ActualWidth, ElementName=theButton}" Margin="0,0,2,0" Text="{Binding Path=FileName, ElementName=root}"/>
    </DockPanel>
</UserControl>

Now I get a compilation error...
The type name 'FileInputBox' does not exist in the type 'FileInputBox.FileInputBox'

It turns out the problem is that the Namespace and the Class names are the same. All I had to do is rename the Class. The problem occurs as the compiler tries to figure out exactly what is being called "root". Seems odd to me.

Wednesday, January 27, 2016

Displaying a message box while a splash screen is visible

If you use a splash screen while your WPF application is initializing you may have noticed a problem when you try to show a messagebox while the splash screen is visible. Perhaps the initialization process failed in Application_Startup.

The problem is that the splash screen is the active window and it is designed to disappear as soon as the first visual element is displayed. By default, message boxes are owned by the active window. You can see what happens. The message box is owned by the splash screen, the splash screen disappears as soon as the message box is displayed, so the message box disappears too.

For me, the problem is made worse because I have an unhandled exception handler defined for the application so any unhandled exception is handled by the same code whether it was raised while the splash screen was visible or later.

Private Sub Application_DispatcherUnhandledException(sender As Object, e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs) Handles Me.DispatcherUnhandledException
    ShowFatal(e.Exception.Message)
End Sub

The ShowFatal method simply displays a MessageBox. When the exception originates while the splash screen is displayed the message box disappears before the user can read it. They launch the application, something flashes, and the application exits. Not cool.

The trick is to know that the MainWindow does not yet exist during initialization. In fact, it is the creation of the main window that makes the splash screen go away. So if the main window is not yet created we display a dummy message box that attaches to the splash screen and causes the splash screen to go away. Then we display the proper message box.

Public Shared Sub ShowFatal(sMsg As String)
   ' If the main window is not yet displayed then show a dummy messagebox
    If Application.Current.MainWindow Is Nothing Then System.Windows.MessageBox.Show("")

    System.Windows.MessageBox.Show(sMsg, "Fatal Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Stop)
    Application.Current.Shutdown(1)

End Sub

Tuesday, January 26, 2016

Getting the client name and IP address from WCF

One of the concerns we have where I work is that users keep sharing logons and passwords. Then later we get support calls such as "The change history says I did this but I wasn't even here that day". So I decided to see what it would take to track IP Addresses.

Obviously the client knows its own IP Address and can send it to the WCF endpoints that actually do all the work. However we have a lot of endpoints and I didn't want to have to change all the contracts so I looked for a way for the end point to know the IP Address of the client that it is talking to. I turns out to be quite easy if you are targeting framework 3.5 or later.

Imports System.ServiceModel
Dim context As OperationContext
Dim prop As Channels.MessageProperties
Dim endpoint As Channels.RemoteEndpointMessageProperty

context = OperationContext.Current
prop = context.IncomingMessageProperties
endpoint = CType(prop(Channels.RemoteEndpointMessageProperty.Name), Channels.RemoteEndpointMessageProperty)

Return endpoint.Address

So this works great, but IP Addresses can change over time as they are released and renewed. Even better would be the client name, but that requires a reverse DNS lookup from the IP Address. Here is the completed code to get the IP Address from the conversation and perform a reverse DNS lookup. If the lookup fails the IP Address is returned instead.

Note that when the client and the WCF endpoint are on the same computer as is the case when you are debugging, the conversation doesn't even use TCP/IP so this all fails and returns "n/a".

Imports System.ServiceModel
Public Shared Function GetClientName() As String

    Dim context As OperationContext
    Dim prop As Channels.MessageProperties
    Dim endpoint As Channels.RemoteEndpointMessageProperty
    Dim addr As Net.IPAddress
    Dim entry As Net.IPHostEntry

    Try
        context = OperationContext.Current
        prop = context.IncomingMessageProperties
        endpoint = CType(prop(Channels.RemoteEndpointMessageProperty.Name), Channels.RemoteEndpointMessageProperty)

        Try
            addr = Net.IPAddress.Parse(endpoint.Address)
            entry = Net.Dns.GetHostEntry(addr)
            Return entry.HostName
        Catch ex As Exception
            Return endpoint.Address
        End Try
    Catch ex As Exception
        Return "n/a"
    End Try
End Function

Wednesday, January 13, 2016

DataGrid SelectedItems collection

I discovered something unexpected while researching a user's complaint today. It make sense, which may explain why it was unexpected.

The user was complaining that when they select several rows in a grid, I wasn't processing them in document number sequence, even though the grid was sorted in document number sequence. Of course, being a typical user, they omitted to mention that they weren't selecting the first row to be processed and then shift-clicking the last row. If you do that (which I always do because I think LOGICALLY) everything works as they expected.

Instead, they were selecting rows randomly. It turns out the the datagrid's selecteditems collection lists the rows in the sequence they were selected, not in the sequence they were displayed. This makes perfect sense now that I think about it.

It actually means that an intelligent user could specify the sequence they want stuff processed in. However, I doubt many developers have the luxury of a user group that is intelligent enough to use such a feature. I know I don't.

I emailed the user back saying they are using the grid selection feature incorrectly and they have to select the first row and shift-click the last row. That shut them up for a while.

One more thing, for the Microsoft people (I'm sure Bill reads my blog religiously). We seriously need a datagrid footer feature. It would be just like the header feature but at the bottom. The hacks I'm seeing online to fake up this feature are ugly.

TTFN

Wednesday, December 16, 2015

SQL Server ALTER COLUMN calculates table width incorrectly

SQL Server 2012

I recently discovered a bug/feature of SQL Server that drove me crazy until a colleague found an article that explained everything. Before I reference the article, read through the following...

Consider this table create statement...
CREATE TABLE WideTable (Col1 int, Col2 char(3000), Col3 char(3000));

Now change the width of Col2 to char(4000) to make the total width = 7004 bytes ...
ALTER TABLE WideTable ALTER COLUMN Col2 char(4000)

You get the error
Msg 1701, Level 16, State 1, Line 2
Creating or altering table 'WideTable' failed because the minimum row size would be 10011, including 7 bytes of internal overhead. This exceeds the maximum allowable table row size of 8060 bytes.

Where does SQL Server get 10011 bytes from? This is a perfectly legal change isn't it? Let's prove it...
CREATE TABLE WideTable2 (Col1 int, Col2 char(4000), Col3 char(3000));

Apparently SQL Server can't do math. That's a little scary. Here's the story...

Let's start off by running a handy query - you might want to save this one.

SELECT  c.name AS column_name, column_id, max_inrow_length,
      pc.system_type_id, leaf_offset
 FROM sys.system_internals_partition_columns pc
    JOIN sys.partitions p
        ON p.partition_id = pc.partition_id
    JOIN sys.columns c
         ON column_id = partition_column_id
            AND c.object_id = p.object_id
WHERE p.object_id=object_id('WideTable');
GO

This query shows the results below.
column_name column_id   max_inrow_length system_type_id leaf_offset
----------- ----------- ---------------- -------------- -----------
Col1        1           4                56             4
Col2        2           3000             175            8
Col3        3           3000             175            3008

Now you can see that there is a four byte overhead before Col1, Col2 starts at offset 8, and Col3 starts at offset 3008. So if we added another column it would have an offset of 6008. A new column with a width of more than about 2050 bytes would exceed the 8060 maximum allowable table row size. Let's try reducing the width of Col2 to 2500 bytes and see what happens.

ALTER TABLE WideTable ALTER COLUMN Col2 char(2500) 

This works. Let's run that query again...
column_name column_id   max_inrow_length system_type_id leaf_offset
----------- ----------- ---------------- -------------- -----------
Col1        1           4                56             4
Col3        3           3000             175            3008
Col2        2           2500             175            8

Look at the column sequence. It changed. It looks as though SQL Server is adding the altered column to the end of the column list and then removing it from the middle. In between those events it looks like there is code that says something like "If the column width is increasing then check that the sum of the columns does not exceed 8060." But it's including the old AND the new widths of the altered column. This check is not performed if the column width is not increasing.

Looks like a bug, smells like a bug, walks like a bug.

The article that helped me on this issue is http://sqlblog.com/blogs/kalen_delaney/archive/2006/10/13/alter-table-will-not-reclaim-space.aspx. It talks about a slightly different issue but is very relevant to this problem.

The solution is obviously to make a structure change that forces the table to be rebuilt. There's a discussion of this at the end of the referenced blog entry.

Monday, October 19, 2015

Binding to a user control property

This is for Framework 4.0.

I have a page with a user control on it. The user control contains a list and I want to display the count of items in the list in a text block on the main page. It was obvious I would need to create a dependency property in the user control, so...

Public Shared ReadOnly CountProperty As Windows.DependencyProperty = Windows.DependencyProperty.Register("AttachmentCount", GetType(Integer), GetType(Attachments), Nothing)

Public Property AttachmentCount As Integer
Get
    Return CInt(GetValue(CountProperty))
End Get
Set(value As Integer)
    SetValue(CountProperty, value) 
End Set
End Property

Now it's a simple matter of binding the textblock to the user control's property, right?

<TextBlock Name="AddAttachmentsCount" Text="{Binding ElementName=AddAttachmentsUserControl, Path=AttachmentCount"/>

Oops, now I'm seeing an error in the output window at run-time...


System.Windows.Data Error: 40 : BindingExpression path error: '(AttachmentCount)' property not found on 'object' ''UserControl' (Name='AddAttachmentsUserControl')'. BindingExpression:Path=AttachmentCount; DataItem='UserControl' (Name='AddAttachmentsUserControl'); target element is 'TextBlock' (Name='AddAttachmentsCount'); target property is 'Text' (type 'String')

Well maybe I can bind the AttachmentCount property to the text property of my textblock as I define the instance of my user control...


<local:Attachments x:Key="AddAttachments" AttachmentCount="{Binding ElementName=AddAttachmentsCount, Path=Text, Mode=TwoWay}"/>

Yay!