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!"