Designing identical forms in WPF using abstract classes

Recently I encountered a very interesting task, which quite often can be encountered in the design of user interfaces. The question, of course, is relatively trivial, but I did not find complete and detailed information on it, so I decided to share my own experience. This article may be useful for Junior developers, as well as for people who are just starting to learn OOP and who have no serious practical experience in programming.

The task of constructing the same type of forms with template logic


The bottom line is that we need to create a certain number of forms equivalent to each other to a certain extent. That is, each of these forms may have the same fields, methods, business logic, but at the same time they will not be absolutely equivalent. Each of them can have its own set of methods, variables, visual styles and other components that are specific to its presentation. As applied to my case, these were forms for creating applications for the performance of production work, despite the fact that each work had an individual set of fields.

What is the most competent way to design such a system so that it is as convenient as possible to use and minimally redundant in implementation? Obviously, a pile of controls for all occasions, some of which can be hidden or deactivated, is far from always a good option. Firstly, over time, the number of parameters can grow to such an extent that the complete set will no longer fit into the window. Secondly, such a system will look extremely overloaded and completely uncomfortable for the user. Based on this, we can say that it will be preferable to use the demarcation of functionality with the separation of components.

When developing several similar forms, the following problem arises. Since most of the methods and fields in each of them coincide, with an isolated implementation of logic for each individual form, as any inexperienced programmer can do, there is a redundancy of parameters and a lot of repeating code. Consequently, with any changes in the structure of objects or in the program logic, each method must be individually corrected, which results in a monotonous copy-paste and a significant loss of time for unnecessary actions, including possible errors when inserting "wrong way".

The use of polymorphism would be a logical decision in such a situation. In order to avoid cases similar to those described in the previous paragraph, in languages ​​that implement the object-oriented programming paradigm, features such as abstract classes — classes containing abstract methods and properties that any descendants inherited from it — have been specially developed. We will take them as a basis in this example.

Practical implementation on WPF


I developed my project specifically on WPF, as it required high flexibility and a very complex form structure. However, the principle of this approach is common to any platforms and languages, therefore it can be freely applied in the Web, mobile development and many more.

To demonstrate the possibility of abstract classes using the user interface as an example, we formulate the original problem as follows:

It is necessary to calculate the total profit from movie rental based on available statistics. In this case, the film can be of two varieties: a feature film or a series. For feature films, profits are calculated based on the total box office grossing from movie distribution. For TV shows - according to the total revenue from TV channels. Based on the resulting data, make a verdict: whether the film turned out to be profitable, if so, what is the profit, if not, what is the loss. Provide for the possibility of changing and saving the calculated parameters.

First, create a Movie class that describes the movie:

public class Movie
{
    public Movie(string Name, byte Type, int Cost, int? Dues, int DuesTV, int DuesExtra, short? CinemaPart, short? DistrPart)
    {
        this.Name = Name;
        this.Type = Type;
        this.Cost = Cost;
        this.Dues = Dues;
        this.DuesTV = DuesTV;
        this.DuesExtra = DuesExtra;
        this.CinemaPart = CinemaPart;
        this.DistrPart = DistrPart;
    }
    public string Name { get; set; }
    public byte Type { get; set; }
    public int Cost { get; set; }
    public int? Dues { get; set; }
    public int DuesTV { get; set; }
    public int DuesExtra { get; set; }
    public short? CinemaPart { get; set; }
    public short? DistrPart { get; set; }
}

Designation of parameters:
  • Name - the name of the picture
  • Type - type, 0 - film, 1 - series
  • Cost - total budget
  • Dues - movie rental profits
  • DuesTV - TV Profit
  • DuesExtra - add. profit (DVD, rental)
  • CinemaPart - Cinema Share of Profit
  • DistrPart - Share of Distributors

We describe the main form containing a drop-down list with a list of films and fill it with four elements:

Markup


public partial class Movies : Window
{
    public Movies()
    {
        InitializeComponent();
        List movies = new List()
        {
            new Movie("Охотники за головами", 0, 100000000, 200000000, 40000000, 10000000, 55, 10),
            new Movie("Сумерки", 0, 160000000, 300000000, 60000000, 20000000, 50, 11),
            new Movie("Подземелье", 1, 6000000, null, 22000000, 2000000, null, null),
            new Movie("Заложники Юпитера", 1, 11000000, null, 4000000, 600000, null, null)
        };
        movieList.ItemsSource = movies;
        movieList.DisplayMemberPath = "Name";
    }
    private void calc_Click(object sender, RoutedEventArgs e)
    {
        if (movieList.SelectedIndex != -1)
        {
            Movie movie = ((Movie)movieList.SelectedItem);
            switch (movie.Type)
            {
                case 0:
                    Film film = new Film(movie);
                    film.ShowDialog();
                    break;
                default:
                    Serial serial = new Serial(movie);
                    serial.ShowDialog();
                    break;
            }
        }
        else
        {
            MessageBox.Show("Выберите кинофильм из списка");
        }
    }
    private void movieList_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
    {
        if (((Movie)movieList.SelectedItem).Type == 0)
            Type.Content = "Фильм";
        else
            Type.Content = "Сериал";
    }
}

The first two instances of the Movie class are movies with a full set of parameters, the last two are TV shows that lack movie distribution data.

The form itself will look like this:



After selecting an item from the list by clicking on the button, a corresponding window should open containing the data for the film.

To conduct basic calculation operations and make changes to the list, create a new abstract class called MovieEdit, inherited from Window, which will describe the general logic for calculating profits and manipulating objects. This is our abstract form. It has no visual representation, but only contains general methods for working with the Movie class, regardless of category and parameters:

public class MovieEdit : Window
{
    protected Movie movie;
    protected void calculate(double cost, double cash, string type)
    {
        double result = (cash - cost) / 1000000;
        if (result > 0)
        {
            MessageBox.Show("Доход от " + type + " \"" + Title + "\":\n" + result + " млн.");
        }
        else
        {
            MessageBox.Show("Убыток " + type + " \"" + Title + "\":\n" + -result + " млн.");
        }
    }
    protected void save(int Cost, int? Dues, int DuesTV, int DuesExtra, short? CinemaPart, short? DistrPart)
    {
        MessageBoxResult view = MessageBox.Show("Сохранить изменения?", "Подтверждение",
            MessageBoxButton.YesNo, MessageBoxImage.Question);
        if (view == MessageBoxResult.Yes)
        {
            movie.Cost = Cost;
            if (Dues != null) movie.Dues = (int)Dues;
            if (CinemaPart != null) movie.CinemaPart = (short)CinemaPart;
            if (DistrPart != null) movie.DistrPart = (short)DistrPart;
            movie.DuesTV = DuesTV;
            movie.DuesExtra = DuesExtra;
            Close();
        }
    }
    protected void cancel()
    {
        MessageBoxResult view = MessageBox.Show("Отменить изменения?", "Подтверждение",
            MessageBoxButton.YesNo, MessageBoxImage.Question);
        if (view == MessageBoxResult.Yes)
        {
            Close();
        }
    }
}

Now we’ll create two forms for showing statistics on films, differing in category: a separate form for films, a separate for series. Each of them will contain three buttons: Calculate, Save and Cancel. At the same time, they will no longer be inherited from Window, but from our abstract MovieEdit class, in order to be able to use the methods created earlier.

Markup


We put a handler on each button and call the corresponding functions of the base class inside it, passing the necessary parameters:

public partial class Film : MovieEdit
{
    public Film(Movie movie)
    {
        InitializeComponent();
        this.movie = movie;
        base.Title = movie.Name;
        cost.Text = movie.Cost.ToString();
        dues.Text = movie.Dues.ToString();
        cinemaPart.Text = movie.CinemaPart.ToString();
        distrPart.Text = movie.DistrPart.ToString();
        duesTV.Text = movie.DuesTV.ToString();
        duesExtra.Text = movie.DuesExtra.ToString();
    }
    private void _calc_Click(object sender, RoutedEventArgs e)
    {
        base.calculate(double.Parse(cost.Text),
            double.Parse(dues.Text) * (100 - double.Parse(cinemaPart.Text) - double.Parse(distrPart.Text)) / 100
            + double.Parse(duesTV.Text) + double.Parse(duesExtra.Text), "фильма");
    }
    private void _save_Click(object sender, RoutedEventArgs e)
    {
        base.save(int.Parse(cost.Text), int.Parse(dues.Text), int.Parse(duesTV.Text),
            int.Parse(duesExtra.Text), short.Parse(cinemaPart.Text), short.Parse(distrPart.Text));
    }
    private void _cancel_Click(object sender, RoutedEventArgs e)
    {
        base.cancel();
    }
}

You also need to make corrections to the constructor markup: instead of the top-level Window tag, we prescribe our MovieEdit class. Otherwise, an assembly error occurs: the visual part and the code of the form must be inherited from the same class, since they are components of one element.


        ...
    



For the second form, do the same actions:

Markup


public partial class Serial : MovieEdit
{
    public Serial(Movie movie)
    {
        InitializeComponent();
        base.Title = movie.Name;
        cost.Text = movie.Cost.ToString();
        duesTV.Text = movie.DuesTV.ToString();
        duesExtra.Text = movie.DuesExtra.ToString();
    }
    private void _calc_Click(object sender, RoutedEventArgs e)
    {
        base.calculate(double.Parse(cost.Text), double.Parse(duesTV.Text) + double.Parse(duesExtra.Text), "сериала");
    }
    private void _save_Click(object sender, RoutedEventArgs e)
    {
        base.save(int.Parse(cost.Text), null, int.Parse(duesTV.Text),
            int.Parse(duesExtra.Text), null, null);
    }
    private void _cancel_Click(object sender, RoutedEventArgs e)
    {
        base.cancel();
    }
}



All the necessary information is available, you can test the project. Now the program automatically determines which form to open. We select any element from the list, click "Show data", calculate the profit according to the entered parameters and see the corresponding result:



Result: as you can see, we managed to create two forms with the same behavior, but different presentation, while avoiding code duplication. The whole logic of working with data fits into three methods in a little more than 30 lines. Now, to change the implementation, it is enough to make adjustments to the concrete abstract method without the need to edit each form separately. This example demonstrates the benefits of abstract classes.

In real practice, the forms, of course, will not be so modest. For example, at the moment I am developing two forms for editing applications, each of which contains a couple of dozen fields and almost the same number of buttons for downloading and downloading files from the database. Naturally, you have to constantly add and make changes, and if after each change you copy the code from one form to another, you will have to finish no earlier than next year. It’s better to spend this time on more useful things, isn't it?) The

source code for the project can be found at .

Also popular now: