Monday, March 26, 2012

Nasty Exceptions with Threaded MVVM Application

Consider to work with the private variable of a property versus the public property itself if you are implementing INotifyPropertyChanged. More specifically, if you have your OnPropertyChanged event in the Set block of your property as this makes a call to the Main Thread of your application to update the view (which you probably already knew).

Here is an example…

This property calls the OnPropertyChanged event every time the property is set.

        Private _salesOrderItemView As DataView = Nothing

        Public Property SalesOrderItemView As DataView

            Get

                Return _salesOrderItemView

            End Get

            Set(ByVal value As DataView)

                _salesOrderItemView = value

                OnPropertyChanged("SalesOrderItemView")

            End Set

        End Property

                                                         

And if you populate the property

SalesOrderItemView = dsTemp.Tables(0).DefaultView



Then add a column

SalesOrderItemView.Table.Columns.Add("NewColumn")



And do a For Each loop

For Each row As DataRow In SalesOrderItemView.Table.Rows

       ‘’Do something

Next



If it is quick enough, you will get a nasty exception.

Why? I am not really sure, but I think it has to do with adding a row and be in the iteration of the For Each loop. It seems to me that if you are adding the row, which will call the OnPropertyChanged event and update the view in the main thread, it will continue to execute the background thread. If it is fast enough to get into the For Each loop while the view is being updated, it will take away the object reference to the view.

Though, if I put a couple of lines of code between the adding a column statement and the For Each loop, the view will have enough time to get updated and no exception will be thrown.

Commenting out the OnPropertyChanged event will also avoid an exception being thrown.
Any comments on this?

Tuesday, March 20, 2012

Short fix to oledb blank values when importing excel file

If you are having trouble importing data from excel file and are receiving blank values, it is due to that the excel drive scans (by default) 8 rows and determines which is the predominant data value. This does not take into consideration that you have set your cell type to General or Numeric.

Also, adding the IMAX=1 in the connection string does not always work.



The upside to this is that you can change the default scan vaule from 8 rows to be between 1 and 16. This is done by adding MaxScanRows=1 to the connection string. Note that you can set it to 0 where it SHOULD scan all the rows, but there is a bug in the ODBC driver and the setting has no effect.




The simple solutions is to insert a blank row under the header and add a blank space as the first character.


Rows to Scan: Excel does not provide ADO with detailed schema information about the data it contains, as a relational database would. Therefore, the driver must scan through at least a few rows of the existing data in order to make an educated guess at the data type of each column. The default for "Rows to Scan" is eight (8) rows. You can specify an integer value from one (1) to sixteen (16) rows, or you can specify zero (0) to scan all existing rows. This is done by adding the optional MaxScanRows= setting to the connection string, or by changing the Rows to Scan setting in the DSN configuration dialog box.

However, due to a bug in the ODBC driver, specifying the Rows to Scan (MaxScanRows) setting currently has no effect.



For example:


· In your eight (8) scanned rows, if the column contains five (5) numeric values and three (3) text values, the provider returns five (5) numbers and three (3) null values.


· In your eight (8) scanned rows, if the column contains three (3) numeric values and five (5) text values, the provider returns three (3) null values and five (5) text values.


· In your eight (8) scanned rows, if the column contains four (4) numeric values and four (4) text values, the provider returns four (4) numbers and four (4) null values.


About IMAX=1
http://support.microsoft.com/kb/194124 

Insure that the data in Excel is entered as text. Just reformatting the Excel column to Text will not accomplish this. You must re-enter the existing values after reformatting the Excel column. In Excel, you can use F5 to re-enter existing values in the selected cell.

NOTE: Setting IMEX=1 tells the driver to use Import mode. In this state, the registry setting ImportMixedTypes=Text will be noticed. This forces mixed data to be converted to text. For this to work reliably, you may also have to modify the registry setting, TypeGuessRows=8. The ISAM driver by default looks at the first eight rows and from that sampling determines the datatype. If this eight row sampling is all numeric, then setting IMEX=1 will not convert the default datatype to Text; it will remain numeric.

You must be careful that IMEX=1 not be used indiscriminately. This is IMPORT mode, so the results may be unpredictable if you try to do appends or updates of data in this mode. 

Tuesday, March 13, 2012

TabItem Header and Content as UserControls in ObservableCollection

Here is a little example project I made to allow the tabitem header to be of a usercontrol and tabitem content to be of another usercontrol. It allows for adding and removing tabitems in the same way internet explorer functions. I have also included a flag button next to the close button on each tab item that you can toggle.

This is a full MVVM application written in VB.Net.




 




Imports System.Collections.ObjectModel
Imports System.ComponentModel

Namespace ViewModel
    Public Class TabControlVM
        Implements INotifyPropertyChanged

#Region "Properties"

        ''Holds collection of tab items
        Private _myTab As ObservableCollection(Of Model.MyTab) = Nothing
        Public Property MyTabCollection As ObservableCollection(Of Model.MyTab)
            Get
                Return _myTab
            End Get
            Set(ByVal value As ObservableCollection(Of Model.MyTab))
                _myTab = value
                OnPropertyChanged("MyTabCollection")
            End Set
        End Property

        ''Selected tab item index
        Private _selectedIndex As Integer = 0
        Public Property SelectedIndex As Integer
            Get
                Return _selectedIndex
            End Get
            Set(ByVal value As Integer)
                _selectedIndex = value
                OnPropertyChanged("SelectedIndex")
            End Set
        End Property

#End Region

#Region "Relay Commands"

        Private _addTab As RelayCommand = Nothing
        Public ReadOnly Property AddTabCommand() As ICommand
            Get
                If _addTab Is Nothing Then
                    Dim commandAction As New Action(Of Object)(AddressOf Me.AddTab)
                    _addTab = New RelayCommand(commandAction)
                End If
                Return _addTab
            End Get
        End Property

        Private _removeTab As RelayCommand = Nothing
        Public ReadOnly Property RemoveTabCommand() As ICommand
            Get
                If _removeTab Is Nothing Then
                    Dim commandAction As New Action(Of Object)(AddressOf Me.RemoveTab)
                    _removeTab = New RelayCommand(commandAction)
                End If
                Return _removeTab
            End Get
        End Property

#End Region

#Region "Constructor"

        ''' <summary>
        ''' <para>Creates a new obeservable collection with tab items for tab control.
        ''' Uses usercontrol for tab item header and content.</para>
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub New()

            ''Create a MyTab collection (tabitems)
            _myTab = New ObservableCollection(Of Model.MyTab)

            ''Create tabitem header object
            Dim tabHeaderObj = New TabHeaderAddUC
            tabHeaderObj.DataContext = Me

            ''Create the Add Tab content object
            Dim tabContentObj As New TabContentAddUC
            tabContentObj.DataContext = Me

            ''Add to collection
            _myTab.Add(New Model.MyTab(tabHeaderObj, tabContentObj, 0))

        End Sub

#End Region

#Region "Add Tab"

        ''' <summary>
        ''' <para>Method which creates a new tabitem object and adds it to MyTab collection.</para>
        ''' </summary>
        ''' <param name="obj">Object of command parameter from view.</param>
        ''' <remarks></remarks>
        Private Sub AddTab(ByVal obj As Object)

            ''Checks the highest MyTab.Index which will be used when assigingin
            ''a new tab item (index + 1)
            Dim i As Integer = 0
            For Each tb As Model.MyTab In MyTabCollection
                If tb.Index > i Then i = tb.Index
            Next

            ''Create tabitem header object
            Dim tabHeaderObj = New TabHeaderUC
            ''Enable if you want to explicitly set view model class here instead of in the view
            ''-----------------------------------------------------------------------------
            tabHeaderObj.DataContext = New ViewModel.TabHeaderVM With {.HeaderText = "Tab item " & i}
            ''-----------------------------------------------------------------------------

            ''Create tabitem content object
            Dim tabContentObj As New TabContentMyContentUC
            tabContentObj.DataContext = Me

            ''Have the add tab on the left side
            ''-----------------------------------------------------------------------------
            ''Add to collection
            'MyTabCollection.Add(New Model.MyTab(tabHeaderObj, tabContentObj, i + 1))
            ''-----------------------------------------------------------------------------

            ''Have the add tab on the right side
            ''-----------------------------------------------------------------------------
            ''Insert to collection
            MyTabCollection.Insert(i, New Model.MyTab(tabHeaderObj, tabContentObj, i + 1))
            ''-----------------------------------------------------------------------------

            ''Set the highest tab item to visible
            SelectedIndex = i

        End Sub

#End Region

#Region "Remove Tab"

        ''' <summary>
        ''' <para>Removes the tab item at specified index.</para>
        ''' </summary>
        ''' <param name="obj">Object of command parameter from view containing the TabItem.</param>
        ''' <remarks></remarks>
        Private Sub RemoveTab(ByVal obj As Object)

            ''Gets the tabitem MyTab.Index from tabitem Tag and
            ''removes the item at this location from the collection
            Dim tabItemObj = DirectCast(obj, TabItem)

            ''Have the add tab on the left side
            ''-----------------------------------------------------------------------------
            'If Not tabItemObj.Tag = 0 Then MyTabCollection.RemoveAt(tabItemObj.Tag)
            ''-----------------------------------------------------------------------------

            ''Have the add tab on the right side
            ''-----------------------------------------------------------------------------
            If Not tabItemObj.Tag = 0 Then MyTabCollection.RemoveAt(tabItemObj.Tag - 1)           
            ''-----------------------------------------------------------------------------

            ''Rebuild the MyTab.Index to match tabitem index
            Dim j As Integer = 1
            For Each item As Model.MyTab In _myTab
                If Not item.Index = 0 Then
                    item.Index = j
                    j += 1
                End If
            Next

            ''Set the highest tab item to visible
            SelectedIndex = j
        End Sub

#End Region

#Region "INotifyPropertyChanged"

        Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

        Protected Overridable Sub OnPropertyChanged(ByVal PropertyName As String)
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))

        End Sub

#End Region

    End Class

End Namespace

Imports System.ComponentModel

Namespace Model
    Public Class MyTab
        Implements INotifyPropertyChanged

#Region "Properties"

        ''Tab item header usercontrol
        Private _header As UserControl = Nothing
        Property Header As UserControl
            Get
                Return _header
            End Get
            Set(ByVal value As UserControl)
                _header = value
                OnPropertyChanged("Header")
            End Set
        End Property

        ''Tab content user control
        Private _content As UserControl = Nothing
        Property Content As UserControl
            Get
                Return _content
            End Get
            Set(ByVal value As UserControl)
                _content = value
                OnPropertyChanged("Content")
            End Set
        End Property

        ''Tab item index in tabcontrol
        Private _indexObj As Integer = 0
        Property Index As Integer
            Get
                Return _indexObj
            End Get
            Set(ByVal value As Integer)
                _indexObj = value
                OnPropertyChanged("Index")
            End Set
        End Property

#End Region

#Region "Constructor"

        ''' <summary>
        ''' <para>Constructor accepting two user controls as parameters for header and content.</para>
        ''' </summary>
        ''' <param name="h">UserControl containing header information.</param>
        ''' <param name="c">UserControl containing content information.</param>
        ''' <param name="i">Integer representing an artificial index of the tabitems.</param>
        ''' <remarks></remarks>
        Public Sub New(ByVal h As UserControl, ByVal c As UserControl, ByVal i As Integer)
            Try
                Header = h
                Content = c
                Index = i
            Catch ex As Exception
                Throw New Exception("Error in MyTab constructor.")
            End Try
        End Sub

#End Region

#Region "INotifyPropertyChanged"

        Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

        Protected Overridable Sub OnPropertyChanged(ByVal PropertyName As String)
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
        End Sub

#End Region

    End Class
End Namespace

Imports System.Collections.ObjectModel
Imports System.ComponentModel

Namespace ViewModel
    Public Class TabHeaderVM
        Implements INotifyPropertyChanged

#Region "Properties"

        ''Set flag image
        Private _imageSource As String = ""
        Public Property ImageSource As String
            Get
                Return _imageSource
            End Get
            Set(ByVal value As String)
                _imageSource = value
                OnPropertyChanged("ImageSource")
            End Set
        End Property

        ''Header text
        Private _headerText As String = ""
        Public Property HeaderText As String
            Get
                Return _headerText
            End Get
            Set(ByVal value As String)
                _headerText = value
                OnPropertyChanged("HeaderText")
            End Set
        End Property

#End Region

#Region "Relay Commands"

        Private _SetTabItemFlagImageCommand As RelayCommand = Nothing
        Public ReadOnly Property SetTabItemFlagImageCommand() As ICommand
            Get
                If _SetTabItemFlagImageCommand Is Nothing Then
                    Dim commandAction As New Action(Of Object)(AddressOf Me.SetTabItemFlagImage)
                    _SetTabItemFlagImageCommand = New RelayCommand(commandAction)
                End If
                Return _SetTabItemFlagImageCommand
            End Get
        End Property

#End Region

#Region "Constructor"

        ''' <summary>
        ''' <para></para>
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub New()

        End Sub

#End Region

#Region "Set Flag Visibility"

        Private Sub SetTabItemFlagImage()
            If ImageSource = "" Then
                ImageSource = "/Images/16x16/Flag-red.png"
            ElseIf ImageSource = "/Images/16x16/Flag-red.png" Then
                ImageSource = "/Images/16x16/Flag-gray.png"
            ElseIf ImageSource = "/Images/16x16/Flag-gray.png" Then
                ImageSource = ""
            End If

        End Sub

#End Region

#Region "INotifyPropertyChanged"

        Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

        Protected Overridable Sub OnPropertyChanged(ByVal PropertyName As String)
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))

        End Sub

#End Region

    End Class

End Namespace