Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wpf reusable window model/template

I'm trying to understand and learn a bit more about WPF.

In a WPF project with 30 windows, all windows must have:

  1. same toolbar (with the buttons: new, edit, delete, view and close)
  2. the same statusbar
  3. each window must have a tabcontrol with 2 TabItem. The contents of the first TabItem can be different according to the window. The second TabItem should always contain a datagrid with different columns according to the window.

The "logical sequence" is:

  1. open the window
  2. the user enters the parameters in the first TabItem
  3. the user clicks the search button
  4. the data are displayed in the datagarid in the second TabItem
  5. the user clicks on new / edit / delete / view
  6. show a window for editing data

The buttons new / edit / view and delete must be enabled only when a row in the gridview is selected. The "logical sequence" is valid for all windows!

In winform you can create a form with the structure to use as a template and create 30 windows that inherit from the model. With custom events in the template, you can understand if user have selected a row in the gridview, understand if the user click a button on the toolbar etc

For example, when the user click on the New button is generated the "NewItem" event that is a custom event, then in the form that inherit from the model when recive the "NewItem" event open the input form

This is the form with the first tab selected

This is the fom with the second tab selected

You can do something similar in WPF?

you can create a form template in WPF and creating windows that inherit from the template?

I hope I was clear and sorry for my bad English

Thanks

like image 929
LucaDev Avatar asked Dec 19 '22 20:12

LucaDev


2 Answers

Ok, I'll try to provide a detailed answer, so this might get a little long, but bear with me.

First of all, if you're working with WPF, it is extremely important to leave behind anything you might be used to from "traditional" technologies such as winforms, and instead, understand and embrace The WPF Mentality.

in WPF, you don't "inherit from a base Window" in order to define the application's functionality, because The UI is NOT the application. The UI is just a nice way to have the end user interact with the application.

Instead, the application's interaction logic and functionality is manifested in some classes called ViewModels, which basically define all the contents and actions to be exposed by the UI.

After that, the UI is "connected" to the ViewModel via DataBinding.

This allows a huge level of reusability, scalability, maintainability, and even testability, because it actually completely separates UI from application/business logic and data.

So, these are the basic steps you would need to perform what you're describing, in WPF:

1 - Basic DataBinding support

since Two-way databinding in WPF requires Property Change Notification, the first thing we do is to create a base "bindable" class to support this basic functionality:

public class Bindable : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}
  • Notice that I'm NOT talking about any UI-specific stuff. I'm only using System.ComponentModel.INotifyPropertyChanged which is part of the basic System.dll .Net Framework assembly and is not WPF-specific.

2 - Parent / Child ViewModels

Your application will require that the parent Window functionality be defined in a base class, while leaving the opportunity to inherit and customize the "child" part for each specific Window type, therefore we will create a base generic ViewModel to define all the common reusable functionality, while also defining a generic "Child" property:

public class ViewModelBase<T>: Bindable where T: Bindable
{
    private T _child;
    public T Child
    {
        get { return _child; }
        set
        {
            _child = value;
            OnPropertyChanged("Child");
        }
    }
}
  • Notice how this is structure is suitable for your requirement:

enter image description here

  • Also notice the basic example of a property with Change Notification, where you raise the OnPropertyChanged() in the property setter.

3 - Commands and Buttons

after that, you need to define the basic functionality for your Toolbar Buttons. Instead of using the traditional Click event handler approach, these are manifested as Commands that are decoupled from the UI, and whose execution logic is defined in the ViewModel rather than code behind.

For that purpose, we will define a basic reusable DelegateCommand:

//Dead-simple implementation of ICommand
//Serves as an abstraction of Actions performed by the user via interaction with the UI (for instance, Button Click)
public class Command : ICommand
{
    public Action Action { get; set; }

    public void Execute(object parameter)
    {
        if (Action != null)
            Action();
    }

    public bool CanExecute(object parameter)
    {
        return IsEnabled;
    }

    private bool _isEnabled = true;
    public bool IsEnabled
    {
        get { return _isEnabled; }
        set
        {
            _isEnabled = value;
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, EventArgs.Empty);
        }
    }

    public event EventHandler CanExecuteChanged;

    public Command(Action action)
    {
        Action = action;
    }
}

Using this command class, we can now define all of our commands in the main ViewModel:

public class ViewModelBase<T>: Bindable where T: Bindable
{
    private T _child;
    public T Child...

    public Command SearchCommand { get; private set; }
    public Command NewCommand { get; private set; }
    public Command EditCommand { get; private set; }
    public Command ViewCommand { get; private set; }
    public Command DeleteCommand { get; private set; }
}

These commands require an Action delegate to indicate what will be performed when they are executed (when the Buttons are clicked). Notice how we begin to actually define functionality, while we haven't even touched any part of the UI yet.

This is how you define the actions:

public class ViewModelBase<T>: Bindable where T: Bindable
{
     //... all of the above.

    //in the constructor:
    public ViewModelBase()
    {
        SearchCommand = new Command(Search);
        NewCommand = new Command(New);
        //... And so on...
    }

    //Actions for the commands:
    private void Search()
    {
        //... your search logic here.
    }

    private void New()
    {
        //... your New logic here...
    }

    //... And so on...
}

Enabling and Disabling Commands: You also mentioned that there will be a DataGrid in the second tab, which will contain the search results, and that will enable/disable some of the buttons.

Notice that the Command class defines and IsEnabled property, which in turn raises the System.Windows.ICommand.CanExecuteChanged event. WPF's commanding engine is capable of listening to this event, and enabling/disabling UI elements accordingly. Therefore, at any time in your application, you can toggle your buttons' state by doing:

NewCommand.IsEnabled = false; //disables the "New" Button
DeleteCommand.IsEnabled = true; //enables the "Delete" Button

4 - The DataGrid

This is the most interesting part.

In abstraction, DataGrid, ListBox, ListView, ComboBox, and all ItemsControls in WPF are controls that display items from a collection, eventually allowing the user to select one or more items.

In WPF, it is common to use the ObservableCollection<T>, because it is a specialized collection type that raises events whenever items are added / removed / cleared from it. WPF's binding engine listens for such events and updates the UI accordingly.

Since we do not know up front what type of items will be shown in the DataGrid, and our ultimate goal is reusability, it is time to add another generic type parameter to our parent ViewModelBase:

public class ViewModelBase<T, TItems>: Bindable where T: Bindable where TItems: class
{
   //... All of the above...
}

Now we can define our Collection property:

public class ViewModelBase<T, TItems>: Bindable where T: Bindable where TItems: class
{
   //... All of the above...

    private ObservableCollection<TItems> _searchResults;
    public ObservableCollection<TItems> SearchResults
    {
        get { return _searchResults; }
        private set
        {
            _searchResults = value;
            OnPropertyChanged("SearchResults");
        }
    }

We also need one property to store the selected item, which in turn will cause the Buttons to be enabled when an item is selected, and disabled when the selection is cleared:

    private TItems _selectedItem;
    public TItems SelectedItem
    {
        get { return _selectedItem; }
        set
        {
            _selectedItem = value;

            NewCommand.IsEnabled = value != null;
            ViewCommand.IsEnabled = value != null;
            DeleteCommand.IsEnabled = value != null;
        }
    }
}

At this point, you do realize to what extent we're actually "programming the UI" without even touching the UI. The above code will make the New, View and Delete Buttons enabled when an item is selected in the DataGrid, and disabled when it is not. However all this functionality is completely decoupled from the UI.

5 - Child widgets (TabItem 1)

At this point we have a completely generic, reusable, base functionality that can be adapted to work with any Item type, also allowing any Child "widget" to be placed inside the parent one.

Also notice how this is a thousand times better than the traditional winforms approach, because:

  • 1 - It is Unit Testable.
  • 2 - the Items collection for the DataGrid is actually generic, which means that you get strongly-typed, compile-time verifiable, Intellisense-enabled coding (as opposed to just a List<object> or the like) without the need for casting or any other poor coding practices.
  • 3 - the UI has not been even defined, which means that you get a huge opportunity for customization. The code we have written so far will work, regardless if the UI contains a DataGrid, or a 3D projection of a flying pink elephant.

Now, it's just a matter of creating the Child widgets that will be placed in each of the 30 different Windows

Say, for example, that you need to show something like this:

enter image description here

The first thing you need to do is to define the ViewModel for that, which would be something like:

public class PersonViewModel: Bindable
{
    public string LastName { get; set; }

    public string FirstName { get; set; }

    public string City { get; set; }

    public string PostalCode { get;set; }
}

Notice that we're NOT raising property change notifications here, because we don't really need Two-way DataBinding. OneWay will suffice. Still, we inherit from Bindable because the Parent ViewModel has a type constraint in the T type parameter.

Then, you need to Create a UserControl:

<UserControl x:Class="WpfApplication1.PersonView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

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

        <Label HorizontalAlignment="Right" Grid.Row="0" Grid.Column="0" Content="Last Name:" />
        <Label HorizontalAlignment="Right" Grid.Row="1" Grid.Column="0" Content="First Name:" />
        <Label HorizontalAlignment="Right" Grid.Row="2" Grid.Column="0" Content="City:" />
        <Label HorizontalAlignment="Right" Grid.Row="3" Grid.Column="0" Content="Postal Code:" />

        <TextBox Grid.Row="0" Grid.Column="1" Margin="2" Text="{Binding LastName}"/>
        <TextBox Grid.Row="1" Grid.Column="1" Margin="2" Text="{Binding FirstName}"/>
        <TextBox Grid.Row="2" Grid.Column="1" Margin="2" Text="{Binding City}"/>
        <TextBox Grid.Row="3" Grid.Column="1" Margin="2" Text="{Binding PostalCode}"/>
    </Grid>
</UserControl>

Notice how the last thing you actually do in WPF is to create the UI.

6 - DataTemplates

In order to let WPF know "which View to use for which ViewModel" you can define DataTemplates at the application-level resources, simply open the App.xaml and add these inside Application.Resources:

<Application x:Class="WpfApplication1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApplication1"
             StartupUri="MainWindow.xaml">

    <Application.Resources>

        <DataTemplate DataType="{x:Type local:PersonViewModel}">
            <local:PersonView/>
        </DataTemplate>
    </Application.Resources>

Notice that you will need to Import your Namespaces in order to be able to reference your classes in XAML.

Ok, I have to go now, go ahead and try these steps. I'll finish my post when I get some more time.

like image 66
Federico Berasategui Avatar answered Jan 02 '23 14:01

Federico Berasategui


So to maybe answer part of your question, I would attempt to extract your toolbar(Search, new, edit..etc..) and status bar out into the mainwindow.xaml. At that point maybe you can do something like:

Have separate views (usercontrols imo) that would be part of a content control within the main window.

<MainWindow>

   <!-- Toolbar -->
   <ContentControl Content="{Binding CurrentTabView}" /> <!-- Where CurrentTabView is of type interface that all your views implement -->

   <!-- Status bar -->

</MainWindow>

ViewModel:

Public IView CurrentTabView { get; set; } //All usercontrol viewmodels implement IView

A question you may have: If my toolbar is in the mainwindow view model how can I talk to other view models that are used for other views?

I would look into something along the lines of MessageSenders/MessageListeners

SendMessage(MessageTokens.SearchToken, new NotificationEventArgs<bool>(null, true)); 

SimpleMvvmToolKit has a nice Message system that all your viewmodels can inherit. Hope this kind of helps.

like image 41
TMan Avatar answered Jan 02 '23 14:01

TMan