6
\$\begingroup\$

I have to do a task hard for me and maybe you can help me. I've tried to design a window dialog to update data from a database view. This database view contains integer, DateTime, string, Time, Boolean properties.

First of all, I load the database view data into a GridControl. The GridControl is readonly not editable. I configure the columns of my GridControl dynamically and this configuration is located in a database table called FieldDescription.

When the end-user doubleclicks on a cell, this window dialog pops up. This dialog shows a Combobox when the type of the column is enum or bit (boolean) and otherwise a TextEdit as default content.Notice for example that I know that a GridColumn represents an enum type because of the FieldDescription. Because the type of this database view property bound to this GridColumn is an integer (specifically the value)

The code of this window dialog is (I use DevExpress controls):

    <!-- DataTemplates -->
    <DataTemplate x:Key="DefaultTemplate">
        <dxe:TextEdit Name="TxtNewValue" Grid.Column="0" Margin="0" 
                      HorizontalAlignment="Stretch" 
                      Text="{Binding TargetValue, UpdateSourceTrigger=PropertyChanged}" 
                      Mask="{Binding TargetFieldDescription.fdValidateExp}" 
                      Loaded="FocusTextEditOnLoad"/>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.Date}">
                <Setter TargetName="TxtNewValue" Property="MaskType" Value="DateTime" />
            </DataTrigger>
            <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.DatTime}">
                <Setter TargetName="TxtNewValue" Property="MaskType" Value="DateTime" />
            </DataTrigger>
            <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.Time}">
                <Setter TargetName="TxtNewValue" Property="MaskType" Value="RegEx" />
            </DataTrigger>
            <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.Num}">
                <Setter TargetName="TxtNewValue" Property="MaskType" Value="RegEx" />
            </DataTrigger>                
        </DataTemplate.Triggers>

    </DataTemplate>
    <DataTemplate x:Key="ComboboxTemplate">
        <dxe:ComboBoxEdit ItemsSource="{Binding EnumItemsSource}" 
                          DisplayMember="Name"
                          ValueMember="Id"
                          IsTextEditable="False"
                          EditValue="{Binding TargetValue, UpdateSourceTrigger=PropertyChanged}"/>
    </DataTemplate>

    <!-- Select a Datatemplate depending on the type of the Enum FieldDataType-->
    <DataTemplate DataType="{x:Type massedit:SimpleFieldVM}">
        <ContentControl Content="{Binding .}">
            <ContentControl.Style>
                <Style TargetType="{x:Type ContentControl}">
                    <Setter Property="ContentTemplate" Value="{StaticResource DefaultTemplate}" />
                    <Style.Triggers>
                        <!-- Without selector, only with DataTriggers select my ContentControl -->
                        <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.ENum}">
                            <Setter Property="ContentTemplate" Value="{StaticResource ComboboxTemplate}" />
                        </DataTrigger>
                        <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.Bit}">
                            <Setter Property="ContentTemplate" Value="{StaticResource ComboboxTemplate}" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    </DataTemplate>

</Window.Resources>

<!-- Layout -->
<Grid x:Name="LayoutRoot" VerticalAlignment="Center">
    <Grid.RowDefinitions>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="20"/>
    </Grid.ColumnDefinitions>

    <ContentControl Name="MainLayout" Content="{Binding}" />
</Grid>

I have only one ViewModel to provide the values to this Dialog Window. My ViewModel has an enum FieldDataType property called TargetFieldType. It contains the type of the doubleclicked GridColumn. This value is an enum (FieldDataType.DatTime, FieldDataType.Time, FieldDataType.Date, FieldDataType.Bit, FieldDataType.Num, FieldDataType.Enum, FieldDataType.String).

The ItemsSource of the ComboBox is bound with the ViewModel property:

public List<EnumItemObject> EnumItemsSource

Depending on the type of the column I prepare the data and I convert an enum of my model, in List<EnumItemObject>, aboolean property also in List<EnumItemObject>(Yes, No). But my ViewModel has to prepare the data depending on the FieldDataType. Maybe I can do more things in the XAML code to save a lot of code to prepare the provided data to my view, or separating my current ViewModel into multiple ViewModels, to do this clearly.

It is difficult to explain the whole task but maybe you can help and tell me if my approach is correct or too difficult. I hope you understand what I mean.

XAML code:

<!-- DataContext definition -->
<Window.DataContext>
    <massedit:SimpleFieldVM/>
</Window.DataContext>

<!-- Behaviors -->
<i:Interaction.Behaviors>
    <behavior:WindowBehavior x:Name="WindowBehavior" OnOk="{Binding DoOnOk}" OnCancel="{Binding DoOnCancel}"/>
</i:Interaction.Behaviors>

<!-- Resources -->
<Window.Resources>
    <!-- DataTemplates -->
    <DataTemplate x:Key="DefaultTemplate">
        <dxe:TextEdit Name="TxtNewValue" Grid.Column="0" Margin="0" 
                      HorizontalAlignment="Stretch" 
                      Text="{Binding TargetValue, UpdateSourceTrigger=PropertyChanged}" 
                      Mask="{Binding TargetFieldDescription.fdValidateExp}" 
                      Loaded="FocusTextEditOnLoad"/>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.Date}">
                <Setter TargetName="TxtNewValue" Property="MaskType" Value="DateTime" />
            </DataTrigger>
            <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.DatTime}">
                <Setter TargetName="TxtNewValue" Property="MaskType" Value="DateTime" />
            </DataTrigger>
            <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.Time}">
                <Setter TargetName="TxtNewValue" Property="MaskType" Value="RegEx" />
            </DataTrigger>
            <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.Num}">
                <Setter TargetName="TxtNewValue" Property="MaskType" Value="RegEx" />
            </DataTrigger>                
        </DataTemplate.Triggers>

    </DataTemplate>
    <DataTemplate x:Key="ComboboxTemplate">
        <dxe:ComboBoxEdit ItemsSource="{Binding EnumItemsSource}" 
                          DisplayMember="Name"
                          ValueMember="Id"
                          IsTextEditable="False"
                          EditValue="{Binding TargetValue, UpdateSourceTrigger=PropertyChanged}"/>
    </DataTemplate>

    <!-- Select a Datatemplate depending on the type of the Enum FieldDataType-->
    <DataTemplate DataType="{x:Type massedit:SimpleFieldVM}">
        <ContentControl Content="{Binding .}">
            <ContentControl.Style>
                <Style TargetType="{x:Type ContentControl}">
                    <Setter Property="ContentTemplate" Value="{StaticResource DefaultTemplate}" />
                    <Style.Triggers>
                        <!-- Without selector, only with DataTriggers select my ContentControl -->
                        <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.ENum}">
                            <Setter Property="ContentTemplate" Value="{StaticResource ComboboxTemplate}" />
                        </DataTrigger>
                        <DataTrigger Binding="{Binding TargetFieldType}" Value="{x:Static eva:FieldDataType.Bit}">
                            <Setter Property="ContentTemplate" Value="{StaticResource ComboboxTemplate}" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    </DataTemplate>

</Window.Resources>

<!-- Layout -->
<Grid x:Name="LayoutRoot" VerticalAlignment="Center">
    <Grid.RowDefinitions>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="20"/>
    </Grid.ColumnDefinitions>

    <ContentControl Name="MainLayout" Content="{Binding}" />

    <dx:UniformStackPanel Grid.Column="1" Orientation="Vertical" ChildSpacing="0">
        <Button Command="{Binding ElementName=WindowBehavior, Path=OnCancelCommand}" Width="18" Height="18" Padding="-1"  Background="Transparent" BorderBrush="Transparent">
            <Button.Content>
                <Image Source="../images/cancel-16x16.png"/>
            </Button.Content>
        </Button>
        <Button Command="{Binding ElementName=WindowBehavior, Path=OnOkCommand}" Width="18" Height="18" Padding="-1"  Background="Transparent" BorderBrush="Transparent">
            <Button.Content>
                <Image Source="../images/ok-16x16.png"/>
            </Button.Content>
        </Button>
    </dx:UniformStackPanel>
</Grid>

ViewModel code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using DevExpress.Data.Filtering;
using DevExpress.Xpo;
using DevExpress.Xpo.DB;
using EVA.Types;
using Model.Eva;
using Model.Eva.EvaDataModelCode;
using Utilities.Eva;

namespace EVAGui.MassEdit
{
    /// <summary>
    /// ViewModel for view SimpleFieldDialog. Provides values that need the view. (From this view can be edited simple types 
    /// (uses defaulttemplate and Enumtemplate) ) 
    /// </summary>
    public class SimpleFieldVM : NotificationObject
    {
        #region properties

        private Action _doOnOk;
        private Action _doOnCancel;
        private object _targetValue;
        private List<EnumItemObject> _enumItemsSource;
        private IList<DbvBigView> _selectedItems;
        private DbvBigView _firstSelectedItem;
        private DbFieldDescription _targetFieldDescription;

        /// <summary>
        /// Action is executed when button ok is clicked
        /// </summary>
        public Action DoOnOk
        {
            get { return _doOnOk; }
            set
            {
                _doOnOk = value;
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// Action is executed when button cancel is clicked
        /// </summary>
        public Action DoOnCancel
        {
            get { return _doOnCancel; }
            set
            {
                _doOnCancel = value;
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// Contains the new value to be saved in database
        /// </summary>
        public object TargetValue
        {
            get { return _targetValue; }
            set
            {
                _targetValue = value;
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// Contains the enum value of the kind of type of the field that should be edited
        /// </summary>
        public FieldDataType TargetFieldType { get; set; }

        /// <summary>
        /// Contains the ItemsSource for Enums
        /// </summary>
        public List<EnumItemObject> EnumItemsSource
        {
            get { return _enumItemsSource; }
            set
            {
                _enumItemsSource = value;
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// Contains the field description of the property of BigView that is being edited
        /// </summary>
        public DbFieldDescription TargetFieldDescription
        {
            get { return _targetFieldDescription; }
            set
            {
                _targetFieldDescription = value;
                if (_targetFieldDescription != null)
                    TargetFieldType = (FieldDataType)_targetFieldDescription.fdDataType;
                else
                    TargetFieldType = FieldDataType.None;
            }
        }

        /// <summary>
        /// Selected elements in the MassEditGrid
        /// </summary>
        public IList<DbvBigView> SelectedItems
        {
            get { return _selectedItems; }
            set
            {
                _selectedItems = value;
                if (_selectedItems.Any())
                    FirstSelectedItem = _selectedItems.ToList()[0];
            }
        }

        /// <summary>
        /// Contains the first selected element. In this view is displayed the data of the first element and
        /// will be applied to all selected items
        /// </summary>
        protected DbvBigView FirstSelectedItem
        {
            get { return _firstSelectedItem; }
            set
            {
                _firstSelectedItem = value;
                if (_firstSelectedItem != null && TargetFieldDescription != null)
                    LookUpViewData();
            }
        }

        #endregion

        #region constructor

        /// <summary>
        /// constructor of this ViewModel. Defines the delegate for Ok Action (save changes)
        /// Ok Action will save the changes in Database
        /// </summary>
        public SimpleFieldVM()
        {
            DoOnOk = () => DbvBigViewUtils.SaveSimpleData(SelectedItems, TargetFieldDescription,
                                                          TargetValue);
            //DoOnOk = () => PersistentDataManager.SaveData();
        }

        #endregion

        /// <summary>
        /// Initializes the ViewModel with the FieldDescription of the field that will be updated and the selected BidView elements in MassEditView
        /// before this dialog pops up
        /// </summary>
        /// <param name="items">Selected BigView elements in MassEditView</param>
        /// <param name="fieldDescription">FieldDescription of the current field</param>
        public void InitializeModelView(IList<DbvBigView> items, DbFieldDescription fieldDescription)
        {
            TargetFieldDescription = fieldDescription;
            SelectedItems = items;
        }

        /// <summary>
        /// Loads the data to be displayed in the view 
        /// </summary>
        private void LookUpViewData()
        {
            var propertyInfo = _firstSelectedItem.GetType().GetProperty(TargetFieldDescription.fdBigViewColumnName);
            if ((propertyInfo != null) && (propertyInfo.GetValue(_firstSelectedItem) != null))
            {
                if ((int) FieldDataType.ENum == TargetFieldDescription.fdDataType)
                    PrepareDataForEnumTemplate(propertyInfo);
                if ((int) FieldDataType.Bit == TargetFieldDescription.fdDataType)
                    PrepareDataForBitTemplate(propertyInfo); // like EnumTemplate
                else if ((int) FieldDataType.Time == TargetFieldDescription.fdDataType)
                    PrepareDataForTimeTemplate(propertyInfo);
                else
                    PrepareDataForDefaultTemplate(propertyInfo);
            }
            else
                TargetValue = String.Empty;
        }

        /// <summary>
        /// Sets the value to the properties for the DefaultTemplate view
        /// </summary>
        /// <param name="propertyInfo"></param>
        private void PrepareDataForTimeTemplate(PropertyInfo propertyInfo)
        {
            var value = propertyInfo.GetValue(_firstSelectedItem);
            var converter = new ConvertTimeDisplay();
            TargetValue = converter.Convert(value, null, null, null);
        }

        /// <summary>
        /// Sets the value to the properties for the BitTemplate view. (similar with EnumTemplate)
        /// </summary>
        /// <param name="propertyInfo">a boolean property</param>
        private void PrepareDataForBitTemplate(PropertyInfo propertyInfo)
        {
            TargetValue = (int)propertyInfo.GetValue(_firstSelectedItem);
            EnumItemsSource = new List<EnumItemObject>();
            EnumItemsSource.Add(new EnumItemObject() { Id = 0, Name = "Nein" });
            EnumItemsSource.Add(new EnumItemObject() { Id = 1, Name = "Ja" });
        }

        /// <summary>
        /// Sets the value to the properties for the DefaultTemplate view
        /// </summary>
        /// <param name="propertyInfo"></param>
        private void PrepareDataForDefaultTemplate(PropertyInfo propertyInfo)
        {
            TargetValue = propertyInfo.GetValue(_firstSelectedItem).ToString();
        }

        /// <summary>
        /// Sets the value to the properties for the EnumTemplate view
        /// </summary>
        /// <param name="propertyInfo"></param>
        private void PrepareDataForEnumTemplate(PropertyInfo propertyInfo)
        {
            TargetValue = (int)propertyInfo.GetValue(_firstSelectedItem);
            if (_targetFieldDescription.fdEnumSource != null)
            {
                switch (_targetFieldDescription.fdEnumSource)
                {
                    case "Station":
                        EnumItemsSource = ConvertListToEnumItemObjectList(_targetFieldDescription.fdEnumSource);
                        break;
                    default:
                        EnumItemsSource = EnumUtil.ConvertEnumToEnumItemObjectList(_targetFieldDescription.fdEnumSource);
                        break;
                }

            }
        }

        /// <summary>
        /// Returns a list of EnumItemObjects from a collection
        /// </summary>
        /// <param name="fdEnumSource">Type of objects to be converted in EnumItemObjects</param>
        /// <returns>The list of EnumItemObjects</returns>
        private List<EnumItemObject> ConvertListToEnumItemObjectList(string fdEnumSource)
        {
            switch (fdEnumSource)
            {
                case "Station":
                    return new XPCollection<DbStation>(EvaApplication.App.MainUnitOfWork, 
                        CriteriaOperator.Parse("1=1"), new[] { new SortProperty("stExternalID", SortingDirection.Ascending) }).
                        Select(s=> new EnumItemObject(){Id=s.stID, Name = s.stGVLsysName}).
                        ToList();
                default:
                    return null;
            }
        }
    }
}
\$\endgroup\$
2
\$\begingroup\$

ViewModel

IMHO #region should be banned; if you need to split your class in regions, it's probably doing too many things. If you need to split a method in regions, it's definitely doing too many things.

Maybe it's just me, but I like seeing the class' constructor as the first thing in the class - with the only possible exception of some private readonly fields, constructor-injected.

Your #region properties includes private fields. I would simply put the backing field close to the property it's backing:

private object _targetValue;

/// <summary>
/// Contains the new value to be saved in database
/// </summary>
public object TargetValue
{
    get { return _targetValue; }
    set
    {
        _targetValue = value;
        OnPropertyChanged();
    }
}

I've never used NotificationObject, but from what I've just read on MSDN it's implementing INotifyPropertyChanged, and by not specifying which property has changed I believe you are invalidating your entire ViewModel, which I find is inefficient - the end result might be the same, but depending on how complex your View is this could mean lots of redundant/useless updates. If TargetValue was modified, notify a change on the TargetValue property.

I think your Action delegates shouldn't start with "do" - they're actions, of course they do something! Given you called them DoOnOk and DoOnCancel, I'd rename them OnOkAction and OnCancelAction.

Your DbvBigViewUtils has a bad smell. It's a static ambient context dependency that's tightly coupled with your ViewModel, which means if you were to try and write unit tests for your class, you would have no way of mocking that dependency and fully controlling your class' behavior.


\$\endgroup\$
0
\$\begingroup\$

I think Controls related code should be in XAML. Ideally in MVVM pattern we provide property values in our viewmodel. But how those property will be displayed is define by XAML. You have told that you are using DevExpress Controls. I have not explored DevExpress Controls, but I bet Gridview available from DevExpress should have different options that you should be able to configure in XAML file.

I will tell you about Telerik Grid(hope it helps). In Telerik Grid we can set how the look and file of Grid columns should be in read only mode and how it should be in editable mode. On similar line you can give a try to solve your business problem.

\$\endgroup\$
  • 1
    \$\begingroup\$ could you provide some examples? \$\endgroup\$ – Malachi Nov 28 '13 at 16:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.