While writing my last article, something occurred to me: what if the app uses an external dll for some functions, how can I package them to send them to the store. Once you are sending an app to the Windows Store, everything it needs to start and run must be packaged, or it won't be certified.
When using an installer to package an app, all you must do is to include the main executable and all the needed files in the install script and the installer will take care of packaging everything. But when your are packaging an app for the Windows Store, there is no such thing as an install script. If you are packaging a Delphi app, just compile it to the Store and voilà!, an appx file is created. With Visual Studio, you can create a packaging project, add all the projects in the solution that you want and it will create an appx file for you.
But sometimes, you need to use an external dll, which you may not have the source code, and it must be packaged with the main executable. In this article, I will show how to package an external dll with the main executable with Delphi and with Visual Studio.
For the article, we will take on the Financial Calculator that we created for the last article and refactor it: the financial functions will be in an external Win32 dll and the UI will be in the main executable. That way, we will do two things:
- Separate the UI and the business rules
- Create an external component that may be used in many situations: we will use it for our two apps - the Delphi and the WPF one. That is a great way to refactor your code when you have stable business rules that you don't want to touch and you need to evolve the UI of your app
Refactoring the Delphi UI
As you can see from this code, the business rules are mixed with the UI, thus making it difficult to understand it and change the code, if it's needed.
For example, all the values are dependent on the text box values and the result is also posted in the result text box. This is bad design and not testable. A better thing would be something like this:
This is cleaner, does not depend on the UI and easier to understand. But it is not testable, yet, because the method is in the code behind for the UI, so to test it you should need to instantiate a new Form1, which is not feasible under automated tests (unless you are doing UI tests, which is not the case). You could move this code to another unit, to allow it to be testable, but it won't be reusable. If you want to use the same code in another program, you should have to copy the unit, with all the problems you may have with that:
- Difficulty to change the code: if you find an error or want to refactor the code, you should fix the same thing in many places
- Impossible to use in programs written in other languages, unless you rewrite the code
The best way in this case is to move the code to an external dll. That way, the code will be both testable and reusable: you can even use the same dll in programs written in other languages with no change.
The first step is to create a new project in the project group, a dynamic library. Save it and call it FinCalcDll. Then add a new unit to it and save it as PVCalculator. You should be asking why am I saving the unit with this name and not as FinancialCalculators. I am doing this because I want to treat each unit as a single class and respect the Single Responsibility Principle. Following that principle, the class should have only one reason to change. If I put all calculators in a single unit (class), there will be more than one reason to change it: any change in any of the calculators will be a reason to change. Then, we can add the first function:
We must use the Math unit, to have the Power function available and declare the function in the Interface section, so it can be visible externally. It must be declared as stdcall to be called by other languages. Create new units and save them as FVCalculator, IRRCalculator and PmtCalculator and add these functions:
In the dpr file, you must export the functions. In the Projects window, select the dll and right-click on it, selecting the View Source option. In the source for the dpr file, add the Exports clause:
Then, in the Unit1 for the executable, make the changes needed to use the new dll functions:
We declare the functions and use them in the OnChange handlers of the textboxes. When you build and run the program, you will see something like this:
That's because the dll is not where it should be, in the same folder of the executable. For that, you must take some steps:
- Build the dll before the executable. If you don't do that, the executable will be built and will use an outdated dll
- Copy the dll after building the executable
For the first step, you need to go to the Projects window, select the dll, right click and select the "Build Sooner" option. That will move the dll up in the project list and will make it to be built before the executable.
For the second step, you need to add a post-build step for the executable and copy the dll to the output dir. For that, you need to select the Project Options and go to Build Events:
There, in the Post-build events, you should add a command to copy the dll to the executable output dir:
One thing must be noted here: you must build the dll and the executable for the same platform. If you build the dll for x64, it won't run on a x86 executable. Once you've done the two steps, you can build all projects and run the executable, it will run the same way as before. Now, we've refactored all the business rules into a dll and we can reuse it in other languages. To show that, we will create a WPF project in C# that will use this dll.
Creating a WPF project that uses the DLL
Go to Visual Studio and create a new WPF project, and name it FinCalcWPF. Then go to solution explorer and add the dll file to the project. When adding the dll, select Add as Link, to avoid to make a physical copy of the dll in the source directory. This way, you are just adding a link to the dll and when it's rebuilt, the new version will be used. In the properties window, select Build Action to None and Copy to Output Directory to Copy if newer.
One thing must be noted here: the dll is for Win32, so the executable should also be for Win32. When you build the WPF app with the default setting (Any CPU), you can't be sure it if will run as a Win32 process:
- For a Win32 operating system, it will run as a Win32 process, so that's ok
- For a Win64 operating system, it may run as a Win32 or Win64 process, depending on your settings. If you go to Project/Options/Build and check the "Prefer 32-bit", it will run as a Win32 process, else it will run as a Win64 process
So, if you don't want any surprises, just change the build from Any CPU to x86 and you will be sure that the program will run with the dll.
Then, in MainWindow.xaml, add this code:
We are adding the four tabs with the boxes, the same way we've added in the Delphi app. The code behind for the window is:
We've declared the functions in the dll and the we used them in the TextChanged event handlers. That will fill the result boxes in the tabs when you fill the input boxes. When you run the program, you will have the same result in both apps:
As you can see, refactoring the code into a dll brings many advantages: the code is not dependent on the UI, it is reusable and, best of all, it is testable. Creating unit tests for the code is a great way to be sure that everything works fine and, if you are making a change, you haven't introduced a bug. Now, we'll add the tests for the dll functions.
Adding tests to the dll
To add tests to the dll we must create a new test project to the group. Right click on the Project group and select "Add new project". Then, select the DUnitX project, and set its settings:
When you click the OK button, Delphi will create a new test project with an unit with sample tests. You need to add the four calculator units to your new project and then, we can create the first test:
We named the unit PvCalculatorTests. Then we add the PVCalculator and Math units to the Uses clause. Then, we set the [TextFixture] attribute to the test class, to tell the test framework that this is a class that will have tests. Then we create a method and decorate it with the [Test] attribute. As this will be a parametrized test, we add the cases with the TestCase attribute.
The test is simple. We will run the test with the parameters (there will always be a negative parameter) and the result must always be NaN, thus pointing an invalid entry. If you run the project you will see something like this:
As you can see, the generated test project is a console app that runs the tests and shows the results. If you want a GUI app for the tests, you should install the TestInsight IDE plugin. As you can see from the image, all tests failed, because we have not checked the input parameters. We can change that in PVCalculator:
Now, when you run the tests, all pass:
Now, we can create more tests for this calculator:
You should note one thing. When you run the tests, you will see that some of them fail:
This is not a failure in our code, but a failure in the test. As we are comparing double values, there are many decimals to compare and that's not what you want. You can change your test to compare the difference to a maximum value. If the difference is greater than the maximum, the test fails:
Now, all tests pass. You can create tests for the other calculators the same way we did for this one. If you are using Delphi Rio and run the tests with debugging, you will see that some tests give a floating point error:
But the tests still pass. That's because there is a bug in Delphi Rio (QC#RSP-19882), where comparisons with NaN return true, while they should return false. This can be solved by changing the tests to:
When you run the tests again, you will see that they will fail. We must change the calculator to solve this:
After this, our dll and its tests are ready to use and can be used in any language that supports Win32 dlls.
Conclusions
We've come a long way from the calculator code mixed with the UI to a new dll with unit tests. This architecture is more robust, reusable and easier to maintain. If we need to make changes to the dll, we are covered by unit tests, that can assure we are not introducing new bugs. And if some bug is found in the functions, it's just a matter of writing a new test that fails, thus making sure of the bug, fix the code and rerun the test, making sure it's passed. Using the Red-Refactor-Green procedure, we have a safety net for changing our code.
The full code for this article is at https://github.com/bsonnino/FinCalcDll