Thursday, June 11, 2015

Tooltips on DataGrid headers

I have a DataGridTextColumn which has a dynamically generated tooltip on each cell (binding the cell's tooltip via a multi-value converter) but I want a static tooltip on the column header that explains what the user is looking at. It turns out to be quite easy once you know how to do it.

Here is the original DataGridTextColumn definition...

<DataGridTextColumn Header="Total Pending" Width="60" Binding="{Binding Path=TotalQuantityPending, StringFormat=0.00;;#}" ElementStyle="{StaticResource TextBlockRightAlign}" IsReadOnly="true">
    <DataGridTextColumn.CellStyle>
        <Style TargetType="DataGridCell" BasedOn="{StaticResource DataGridCell}">
            <Setter Property="ToolTip">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource ConversionConverter}">
                        <Binding Path="UORCode"/>
                        <Binding Path="UOICode"/>
                        <Binding Path="Conversion"/>
                        <Binding Path="TotalQuantityReturned"/>
                    </MultiBinding>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGridTextColumn.CellStyle>
</DataGridTextColumn>

Now to add a static tooltip to the header we take advantage of the fact that the header can be any content control. So we make it a textblock with it's own tooltip. Simple when you think of it.

<DataGridTextColumn Width="60" Binding="{Binding Path=TotalQuantityPending, StringFormat=0.00;;#}" ElementStyle="{StaticResource TextBlockRightAlign}" IsReadOnly="true">
    <DataGridTextColumn.Header>
        <TextBlock Text="Total Pending" ToolTip="Total received on this line on all Pending receipts."></TextBlock>
    </DataGridTextColumn.Header>
    <DataGridTextColumn.CellStyle>
        <Style TargetType="DataGridCell" BasedOn="{StaticResource DataGridCell}">
            <Setter Property="ToolTip">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource ConversionConverter}">
                        <Binding Path="UORCode"/>
                        <Binding Path="UOICode"/>
                        <Binding Path="Conversion"/>
                        <Binding Path="TotalQuantityReturned"/>
                    </MultiBinding>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGridTextColumn.CellStyle>
</DataGridTextColumn>

Tuesday, June 2, 2015

Daisy-chaining converters

For WPF 4.0. Credit to Gareth Evans.

I have a converter that returns true if the current user has the specified security code. I have another converter that returns Visible if I pass in a true value otherwise Collapsed. I have a menu control that I want to only show if the user has a specified security code. I wish I could join my two converters so I didn't have to write a third one. Well it turns out I can...

Let's start with the two converters I have already written. Note I removed error handling for simplicity.

<ValueConversion(GetType(String), GetType(Boolean))>
Public Class SecurityToBooleanConverter
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.Convert

        Return Application.Current.Properties.User_SecurityCodes.ContainsKey(parameter) 
    End Function

    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New System.NotImplementedException("SecurityConverter")
    End Function
End Class

<ValueConversion(GetType(Boolean), GetType(Visibility))>
Public Class BooleanToVisibilityConverter
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.Convert

        Return IIf(CBool(value), Visibility.Visible, Visibility.Collapsed)

    End Function

    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New NotImplementedException
    End Function
End Class

In my resources I have created StaticResources like this...
    <local:SecurityToBooleanConverter x:Key="SecurityToBooleanConverter"/>
    <local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>

Now here is the clever bit. In my list of converter classes I add the following generic code. This is the new class that will allow me to string as many converters together as I need.

Public Class ValueConverterGroup
    Inherits Collections.Generic.List(Of IValueConverter)
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.Convert
        Return Me.Aggregate(value, Function(current, converter) converter.Convert(current, targetType, parameter, culture))
    End Function

    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New NotImplementedException()
    End Function
End Class

Now I add a resource that describes how I want to string the converters together. In my example I want to convert from a security code to a boolean and then to a visibility. The new resource to do this looks like this. It could be defined globally in the Application.XAML or on a single page's XAML.

    <local:ValueConverterGroup x:Key="SecurityToVisibilityConverter">
        <local:SecurityToBooleanConverter></local:SecurityToBooleanConverter>
        <local:BooleanToVisibilityConverter></local:BooleanToVisibilityConverter>
    </local:ValueConverterGroup>

When InitializeComponent finds this resource it creates an instance of the ValueConverterGroup and populates the list of converters with a SecurityToBooleanConverter and a BooleanToVisibilityConverter. The ValidConverterGroup has a very clever use of Aggregate that essentially calls each converter in turn passing the value output from the previous converter and with the parameter and culture that was passed to the ValueConverterGroup.

The binding of my control's visibility looks like this...

    <MenuItem Header="Requisitions" Visibility="{Binding Converter={StaticResource SecurityToVisibilityConverter}, ConverterParameter='Requisition'}"/>

The only drawback I see with this solution is that you cannot pass different parameters to different steps in the conversion.

Thursday, May 28, 2015

Binding properties of DataGridColumns

For WPF 4.0 or later

Similar to my earlier blog entry on binding the ItemsSource of a DataGridComboBoxColumn, it is also difficult to bind other properties of all flavors of DataGridColumns. I recently wanted to be able to disable a DataGridColumn when a property was set in the window's DataContext. I was using MVVM at the time but this solution works more generically.

I Googled the problem and found several possible solutions, but the one that caught my eye was to create an invisible ContentControl and create a static resource that referenced it. You set the DataContext of the FrameworkElement to the View-Model class (or the code behind) and then reference the static resource in the dependency object's binding. Once you have defined the Framework Element and the StaticResource once on a page, you can use it in multiple places on that page.

There are other techniques that solve the problem solution wide, but they require a lot more up-front work.

I'll offer my solution using a property in the code-behind. MVVM programmers will know how to tweak it for their needs.

Consider the following window that has a data grid with one column. There is also a CheckBox that can be checked to put the grid's Name column into read-only mode. Here is the XAML and code behind.

<Window x:Class="BindingDataGridColumn.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}}">
    <Grid>
        <CheckBox Content="Is ReadOnly?" IsChecked="{Binding IsReadOnly}"></CheckBox>
    <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Names}" Margin="0,40,0,0">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Name" Binding="{Binding Path=.}" IsReadOnly="{Binding IsReadOnly}"></DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>
    </Grid>
</Window>
----------------------------------------------------------------------------
using System;
using System.Windows;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace BindingDataGridColumn
{
    public partial class MainWindow : Window, INotifyPropertyChanged
            {
        public ObservableCollection<String> Names { get; set; }

        private Boolean _IsReadOnly = false;
        public Boolean IsReadOnly
        {
            get { return _IsReadOnly; }
            set 
            {
                if (value != _IsReadOnly)
                {
                    _IsReadOnly = value;
                    NotifyPropertyChanged("IsReadOnly");
                }
            }
        }

        public MainWindow()
        {
            Names = new ObservableCollection<String> { "Bob", "Mike" };
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String PropertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
        }
    }
}


When we run this application we see we can modify the names even if we check the Is Readonly CheckBox. Also, when we look at the Output window we see the familiar message...

System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=IsReadOnly; DataItem=null; target element is 'DataGridTextColumn' (HashCode=45828630); target property is 'IsReadOnly' (type 'Boolean')

To solve this problem we need to do three things...
    1. Add a resource to the Window that has the same data context as the window...
      <FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
    2. Add a ContentControl and make it invisible. Its content will be the FrameworkElement we just defined, so its data context will be the same as the window...
      <ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>
    3. Use the new ProxyElement as the source in the DataGridColumn's binding. You can re-use the ProxyElement on all DataGridColumns that need it.
      Source={StaticResource ProxyElement}
So now the XAML looks like this. Note there are no code changes required...

<Window x:Class="BindingDataGridColumn.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}}">
    <Window.Resources>
        <FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
    </Window.Resources>
    <Grid>
        <ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>
        <CheckBox Content="Is ReadOnly?" IsChecked="{Binding Path=IsReadOnly}"></CheckBox>
    <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Names}" Margin="0,40,0,0">
        <DataGrid.Columns>
                <DataGridTextColumn Header="Name" Binding="{Binding Path=.}" IsReadOnly="{Binding Path=DataContext.IsReadOnly, Source={StaticResource ProxyElement}}"></DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>
    </Grid>
</Window>

Note we had to change the Binding Path because the Framework Element does not have an IsReadOnly property, but its DataContext does.

Sunday, May 17, 2015

Running Report Manager on newly installed Reporting Services

Sometime Microsoft seems intent on pissing off as many people as possible Their attitude towards this particular problem is exceptionally arrogant and dismissive even for them.

I just installed SQL Server 2014 Reporting Services on my personal development computer and, after configuring it, I went to Report Manager by launching IE and going to http://localhost/Reports. The browser prompted me to authenticate, which I did, and got an error message saying I don't have the required access rights.

This is what I had to do to grant myself (an administrator) rights to use Report Manager in sixteen easy steps.

1. Enable the built-in administrator account
    Run CMD as administrator and enter "NET USER administrator /active:yes"

2. Pray you remember the administrator password

3. Switch user to the administrator

4. Start IE as administrator and go to "localhost/Reports"

5. Click on Site Settings in the top right corner

6. Click on Security in the left column

7. Click [New Role Assignment]

8. Type my domain\user name and check all the checkboxes

9 . Click [OK]

10. Click on Home in the top right corner

11. Click [Folder Settings]

12. Click [New Role Assignment]

13. Type my domain\user name and check all the checkboxes

14. Click [OK]

15. Close the browser and switch back to my user

16. Prevent IE from re-authenticating when you go to Report Manager
     Tools -> Internet Options -> Security -> Local Intranet -> Sites -> Advanced
     Add http://localhost to the list of Intranet sites

Now Microsoft's attitude is that they granted access to the administrator account and that's good enough. It isn't. How hard would it have been to grant site and root folder access rights to the account that installed Reporting Services? This would have solved all these issues and would not have caused a security issue because the installing account can administer reporting services anyway - just not report manager.

Sometimes I find myself becoming confrontational with my users when I think they're being lazy or unreasonable. When I feel this coming on - I really must say to myself "Don't be Microsoft!"

Monday, April 20, 2015

Putting a DatePicker in a datagrid

This is such a common requirement I'm surprised Microsoft hasn't added a DataGridDatePickerColumn but they haven't so every time we want a DatePicker in a DataGrid we have to go through the convolutions below...

<DataGrid ItemsSource="{Binding Path=Events}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="Event Date" Width="200">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Path=EventDate,StringFormat=MM/dd/yyyy}"></TextBlock>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <DatePicker SelectedDate="{Binding Path=EventDate}"></DatePicker>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

Wednesday, April 15, 2015

Cannot find governing FrameworkElement or FrameworkContentElement for target element

I wrote this blog entry because I am fed up with having to remember the solution from scratch every time I get this error message. I happens whenever I try to set the ItemSource for a DataGridComboBoxColumn. Because data grid columns are not in the Visual Tree they don't inherit the data context from the page.

So the XAML below will always fail and you will see "Cannot find governing FrameworkElement or FrameworkContentElement for target element" in the output window. The problem is that the ItemsSource has no DataContext to work in.

<DataGridComboBoxColumn Header="Owner" Width="100" ItemsSource="{Binding Path=Owners}" DisplayMemberPath="OwnerName" SelectedValuePath="OwnerID" SelectedValueBinding="{Binding Path=OwnerID}"></DataGridComboBoxColumn>


There are many solutions out there on the web, but the easiest and most logical for me is to create a CollectionViewSource resource and reference that. To implement the XAML above, simply add the resource...

<Page.Resources>
    <CollectionViewSource x:Key="Owners" Source="{Binding Owners}"/>
</Page.Resources>

... and change the ItemsSource...

ItemsSource="{Binding Source={StaticResource Owners}}"

Sunday, April 12, 2015

MVVM Part 7

Making the DataGrid editable

To make the DataGrid editable we take off the IsReadOnly attribute, set CanUserAddRows and CanUserDeleteRows true, and set the EmployeeID read only. The new DataGrid definition looks like this...

<DataGrid Name="dg" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding Employees}" AutoGenerateColumns="False" CanUserAddRows="True" CanUserDeleteRows="True">
    <DataGrid.Columns>
        <DataGridTextColumn Header="ID" Binding="{Binding Path=EmployeeID}" IsReadOnly="True"/>
        <DataGridTextColumn Header="First Name" Binding="{Binding Path=FirstName}"></DataGridTextColumn>
        <DataGridTextColumn Header="Last Name" Binding="{Binding Path=LastName}"></DataGridTextColumn>
    </DataGrid.Columns>
</DataGrid>

If you run the application now you can modify Billy Bee and click [Save]. The changes are saved to the database. Try adding a new name in the blank row and clicking [Save]. Hmm - the new employee isn't saved to the database.

It turns out that we have to work harder to implement adding and removing rows. We need to provide an event handler for the ObservableCollection's CollectionChanged event to add new rows and remove deleted rows from the Model's employee table.

    1. At the end of the constructor add the following line of code...
        AddHandler Employees.CollectionChanged, AddressOf EmployeesChanged

    2. The event handler looks like this. Add it somewhere in the ViewModel class.
    Protected Sub EmployeesChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
        Select Case e.Action
            Case NotifyCollectionChangedAction.Add
                For Each Emp As Employee In e.NewItems
                    Model.Employees.Add(Emp)
                Next
            Case NotifyCollectionChangedAction.Remove
                For Each Emp As Employee In e.OldItems
                    Model.Employees.Remove(Emp)
                Next
        End Select
    End Sub