Finalizing the application

The application we have created so far is fully functional, but misses a bit of the “magic”. Below are some additional steps that might make your application more appealing and more user friendly. Of course you can go as far as you want by creating custom animations and such, but this guide focuses purely on making the basics more appealing.

Adding validation

Adding validation with Catel is extremely easy. There are two flavors to pick from, but they work exactly the same (since both the models and view models internally derive from ValidatableModelBase). To add validation to the Person model, use this code:

protected override void ValidateFields(List<IFieldValidationResult> validationResults)
{
    if (string.IsNullOrWhiteSpace(FirstName))
    {
        validationResults.Add(FieldValidationResult.CreateError(FirstNameProperty, "The first name is required"));
    }

    if (string.IsNullOrWhiteSpace(LastName))
    {
        validationResults.Add(FieldValidationResult.CreateError(LastNameProperty, "The last name is required"));
    }
}

The validation for the Family model is very easy as well:

protected override void ValidateFields(List<IFieldValidationResult> validationResults)
{
    if (string.IsNullOrWhiteSpace(FamilyName))
    {
        validationResults.Add(FieldValidationResult.CreateError(FamilyNameProperty, "The family name is required"));
    }
}

Note that this validation code can be used in both the model and/or the view models

Adding behaviors to enable double-click on the list boxes

The user must manually click the Edit buttons in the editable views to edit a specific model. To make it easier for the user, we can enable double click to command behaviors. To do so, navigate to the MainWindow and add this to the ListBox definition:

<ListBox x:Name="listBox" ItemsSource="{Binding Families}" SelectedItem="{Binding SelectedFamily}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid>
                <i:Interaction.Behaviors>
                    <catel:DoubleClickToCommand Command="{Binding ElementName=listBox, Path=DataContext.EditFamily}" />
                </i:Interaction.Behaviors>

                <views:FamilyView DataContext="{Binding}" />
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

The same goes for the FamilyWindow:

<ListBox x:Name="listBox" ItemsSource="{Binding Persons}" SelectedItem="{Binding SelectedPerson}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid>
                <i:Interaction.Behaviors>
                    <catel:DoubleClickToCommand Command="{Binding ElementName=listBox, Path=DataContext.EditPerson}" />
                </i:Interaction.Behaviors>

                <views:PersonView DataContext="{Binding}" />
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
 

Note that the xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" must be added in order for the code above to compile

Adding search functionality to the main window

A functionality that is needed in a lot of applications is search functionality. To implement this we will need to modify the MainWindowViewModel. Below are the steps required to implement search functionality.

Adding additional properties to the view model

Lets start by adding the additional properties required to implement searching in the MainWindowViewModel:

/// <summary>
/// Gets the filtered families.
/// </summary>
public ObservableCollection<Family> FilteredFamilies
{
    get { return GetValue<ObservableCollection<Family>>(FilteredFamiliesProperty); }
    private set { SetValue(FilteredFamiliesProperty, value); }
}

/// <summary>
/// Register the FilteredFamilies property so it is known in the class.
/// </summary>
public static readonly PropertyData FilteredFamiliesProperty = RegisterProperty("FilteredFamilies", typeof(ObservableCollection<Family>));

/// <summary>
/// Gets or sets the search filter.
/// </summary>
public string SearchFilter
{
    get { return GetValue<string>(SearchFilterProperty); }
    set { SetValue(SearchFilterProperty, value); }
}

/// <summary>
/// Register the SearchFilter property so it is known in the class.
/// </summary>
public static readonly PropertyData SearchFilterProperty = RegisterProperty("SearchFilter", typeof(string), null, (sender, e) => ((MainWindowViewModel)sender).UpdateSearchFilter());

Note that this property contains an additional change callback function which will be called when the property has changed.

Add the following import to the view model. You will needed because native ObservableCollection class does not support ReplaceRange()

using Catel.Collections;

Add this method to the view model:

/// <summary>
/// Updates the filtered items.
/// </summary>
private void UpdateSearchFilter()
{
    if (FilteredFamilies == null)
    {
        FilteredFamilies = new ObservableCollection<Family>();
    }

    if (string.IsNullOrWhiteSpace(SearchFilter))
    {
        FilteredFamilies.ReplaceRange(Families);
    }
    else
    {
        var lowerSearchFilter = SearchFilter.ToLower();
        FilteredFamilies.ReplaceRange(from family in Families
                                      where !string.IsNullOrWhiteSpace(family.FamilyName) && family.FamilyName.ToLower().Contains(lowerSearchFilter)
                                      select family);
    }
}

Then, add this code to the OnAddFamilyExecute function:

private async Task OnAddFamilyExecuteAsync()
{
    var family = new Family();

    // Note that we use the type factory here because it will automatically take care of any dependencies
    // that the FamilyWindowViewModel will add in the future
    var typeFactory = this.GetTypeFactory();
    var familyWindowViewModel = typeFactory.CreateInstanceWithParametersAndAutoCompletion<FamilyWindowViewModel>(family);
    if (await _uiVisualizerService.ShowDialog(familyWindowViewModel) ?? false)
    {
        Families.Add(family);
        UpdateSearchFilter();
    }
}

Last but not least, add this to the InitializeAsync method after the Families is set from the IFamilyService

protected override async Task InitializeAsync()
{
    var families = _familyService.LoadFamilies();
    Families = new ObservableCollection<Family>(families);

    UpdateSearchFilter();
}

Adding the search functionality to the view

Replace the xaml of the main window by the following content:

 <Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="100" />
    </Grid.ColumnDefinitions>

    <Grid Grid.Row="0" Grid.ColumnSpan="2">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Label Grid.Row="0" Grid.Column="0" Content="Filter:" />
        <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SearchFilter}">
            <i:Interaction.Behaviors>
                <catel:UpdateBindingOnTextChanged UpdateDelay="500" />
            </i:Interaction.Behaviors>
        </TextBox>
    </Grid>

    <ListBox Grid.Row="1" Grid.Column="0" x:Name="listBox" ItemsSource="{Binding FilteredFamilies}" SelectedItem="{Binding SelectedFamily}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <i:Interaction.Behaviors>
                        <catel:DoubleClickToCommand Command="{Binding ElementName=listBox, Path=DataContext.EditFamily}" />
                    </i:Interaction.Behaviors>
                    <views:FamilyView DataContext="{Binding}" />
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    <StackPanel Grid.Row="1" Grid.Column="1">
        <Button Command="{Binding AddFamily}" Content="Add..." />
        <Button Command="{Binding EditFamily}" Content="Edit..." />
        <Button Command="{Binding RemoveFamily}" Content="Remove" />
    </StackPanel>
</Grid>

Contributions

We would like to thank the following contributors:

Want to contribute to the documentation? We have a guide for that!


Questions

Have a question about Catel? Use StackOverflow with the Catel tag!