Codebasis als Dateien ins Arbeitsrepo statt als Gitlink

QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
@@ -0,0 +1,190 @@
# How to create scripts
In order to do any changes over some database table, there are couple of things you need to do first.
## Reserve the script number
The .excel sheet can be found in Teams `c-entron Entwickler` group in the files tab `Datenbankupdate 2 1.xlsx`.
What you have to do there is just write a number - known as a Script number, which has to be for one bigger than the
number used before.
Enter the date of the day when the script number is being reserved. Write a short description about what the script is.
Enter the version from which the change will be included and in the last column your own name.
![Reserve script number](../../../.attachments/How-to-create-scripts/ReserveScriptNumber.png)
> Note: If there are more scripts that have to be executed, all of them can use the same Script number.
## Writing your script method class
Navigate to `Centron.BL/Administration/Scripts/ScriptMethods/Scripts` and create a class with the name `ScriptMethod<x>.cs` where x is your script number.
In the class you have to implement `BaseScriptMethod` and with that the property `ApplicationVersion` and the method `GetSqlQueries()`.
`ApplicationVersion` should be the current version and only ever change the `Buildversion` part (the one marked with <y> in the example).
`GetSqlQueries()` should `yield return` your sql script(s) - SEE NEXT SECTION BEFORE WRITING IT.
```csharp
internal class ScriptMethod<x> : BaseScriptMethod
{
public override Version ApplicationVersion => new(2, 0, <y>, 0);
public override IEnumerable<string> GetSqlQueries()
{
yield return "<here goes your script>";
OR
yield return ScriptHelpers.AddColumnIfNotExists("<your table>", "<your column>", ..);
}
}
```
## Writing your script
There are 2 main ways to write your sql scripts, depending on your needs.
### Using ScriptHelpers
For certain tasks the `ScriptHelpers.cs` will generate the actual sql for you and you just have to call the correct method.
Here are some of the main tasks you can use it for, but check for yourself if scripthelpers can help you.
- `AddColumnIfNotExists()`
- `AddTableIfNotExists()`
- `AddRightIfNotExists()`
- `AddIndexIfNotExists()`
- `AddForeignKeyIfNotExists()`
*If there is a scripthelper method that can help you, you really should use it.*
***Using a scripthelper does not exonerate you from testing your script!***
See (disregard the properties in these examples):
ScriptMethod11355.cs
ScriptMethod11358.cs
### Plain SQL
This is used for everything not covered by `ScriptHelpers` and allows you to just write your own scripts.
If you feel like what you're writing can be automated relativly easily, think about adding method(s) to `ScriptHelpers` so it's reusable.
See (disregard the properties in these examples):
ScriptMethod11359.cs
ScriptMethod11364.cs
### Writing a C# script
If you need to or prefer to write data migration using C# instead of pure SQL, you can do so by overriding the `ExecuteScript()` method instead.
```csharp
public override Result ExecuteScript(DAOSession session)
{
// your code here
return Result.Success();
}
```
You can use the `DAOSession` to interact with the database while having access to the full C# codebase.
If you need both C# and SQL, it is recommended to separate the migration into two separate scripts.
## Adding comments
Depending on the complexity of your script you either should (if less complex) or must (if complex) add a comment on what changed, why and for views it'd be nice to have a 'this script supercedes script <xyz>' with xyz being the last change to that view.
You can mostly reuse the text from `Datenbankupdate 2 1.xlsx` here.
# Legacy ways
The below section is an old way to write script methods and should no longer be used.
## Write the script
In c-entron.NET solution, under the `Centron.BL.Administration.Scripts.ScriptMethods.SqlStatements` can be found a file named
SQLScriptCollection`X`.xaml, where `X` represents the latest collestion of scripts and is the file that is supposed to be edited.
By scrolling to the bottom, you can find templates that are being used:
```xml
<Script Name="" ScriptNumber="">
<Create>
<![CDATA[ { Create Query } ]]>
</Create>
<Alter>
<![CDATA[ { Alter Query } ]]>
</Alter>
</Script>
```
```xml
<Script Name="" ScriptNumber="">
<UpdateQuery>
<![CDATA[ { Update Query } ]]>
</UpdateQuery>
</Script>
```
#### Which template to use?
First template is used to do manipulations with views, functions and store procedures.
Second template is supposed to be used for any table create or update actions, for example: add table, add/remove new column,
add/delete data, drop,...
For any table updates, especially if it's about adding or removing column or adding the whole new table, you should
do a check first if the column or table exists and only then the rest of the query should be executed.
> :exclamation: New scripts should be added at the bottom of the file, before the templates. :exclamation:
> :exclamation: All database changes should be tested by using local or test database before commiting any changes. :exclamation:
## Write new class
For every database change, there has to be a new script method class created. In c-entron.NET solution, under the
`Centron.BL.Administration.Scripts.ScriptMethods` can be found a file named `ScriptMethodsCollection.cs`. In this
file a new internal class has to be created, which contains the script number in the name, eg. `ScriptMethod10806`.
Here is a basic template for it:
```csharp
internal class ScriptMethod10806 : BaseScriptMethod
{
public override int ScriptNumber => 10806;
public override Version ApplicationVersion => new Version(2, 0, 1912, 0);
public override ScriptMethodKind MethodKind => ScriptMethodKind.TableManipulation;
public override ScriptCollectionSource ScriptCollection => ScriptCollectionSource.SQLScriptCollection4;
public override IEnumerable<string> GetScripts()
{
yield return "SomeScriptName";
}
}
```
> Note: New script method class should be added at the bottom of the file.
There are few things that need to be considered before blindly using this template. Depending on what the script is about,
you will have to do some changes to the template.
If your script is a creation or change of view, then you are supposed to do the override of `GetViews()` method. If the script has to do
something with triggers, then you should use `GetTriggers()`, or based on what the script is about just use the propper override method.
All of the methods provided can be found in `BaseScriptMethod.cs` class.
```csharp
public override IEnumerable<string> GetViews()
{
yield return "SomeScriptName";
}
```
Also, one other thing that should be considered, while using the template, is the `ScriptMethodKind`. Depending on what the action
of the script is, the proper `ScriptMethodKind` should be used. There are several of them:
- TableManipulation - this kind is very self explanatory, includes any table change/update
- SystemData - inserting or updating data in the table
- Script
- WithoutTransaction
After doing all those steps, the best way to test if the script is going to execute without any issues is to actually
start the web servie, but with a small change.
In c-entron.NET solution, under `solution items` folder, there is a file named `AssemblyVersionInfo.cs`
which contains the versions.
Normally, the versions are set to `1.0.0.0`. For the testing purposes or just to execute scripts that were previously added
and keep your local database in sync, you should change the versions to `3.0.0.0`.
Save it and start the web service alone or in combination with the UI (doesn't really matter).
In the console window, you can track what scripts were executed and if the execution was successful, otherwise you will know
that something went wrong because it will be displayed in red.
> :exclamation: After testing return the version number to `1.0.0.0` or undo changes done to `AssemblyVersionInfo.cs` file. :exclamation:
@@ -0,0 +1,144 @@
# Centron SQL Server Database Conventions
This document outlines the standardized conventions for SQL Server database tables in the Centron system. These conventions ensure consistency across the database schema and enable efficient data management.
## 1. Table Structure
### Schema Convention
- All database objects must be created in the `dbo` schema
- Always reference tables and other objects using the `[dbo]` schema prefix
- Example: `[dbo].[TableName]`
### Primary Key Convention
Every table must include:
- A primary key column named `I3D` (ID 3develop)
- Defined as `int IDENTITY(1,1) NOT NULL`
- Configured as a clustered primary key index
Example:
```sql
CREATE TABLE [dbo].[TableName](
[I3D] [int] IDENTITY(1,1) NOT NULL,
-- other columns
CONSTRAINT [PK_TableName] PRIMARY KEY CLUSTERED
(
[I3D] ASC
)
)
```
### Foreign Key Convention
- Foreign key columns must end with the suffix `I3D`
- The prefix before `I3D` identifies the referenced table
- Example: `AccountI3D` references the `Account` table's `I3D` column
## 2. Standard Tracking Columns
### Creation Tracking
Include in all tables:
- `CreatedByI3D` [int] NOT NULL - References the Personal.I3D of the user who created the record
- `CreatedDate` [datetime2(2)] NOT NULL - Timestamp when the record was created
### Modification Tracking
Include in all tables:
- `ChangedByI3D` [int] NOT NULL - References the Personal.I3D of the user who last modified the record
- `ChangedDate` [datetime2(2)] NOT NULL - Timestamp when the record was last modified
### Deletion Tracking
Implement soft delete pattern with:
- `IsDeleted` [bit] NOT NULL - Flag indicating if the record is deleted (true) or active (false)
- `DeletedByI3D` [int] NULL - References the Personal.I3D of the user who deleted the record
- `DeletedDate` [datetime2(2)] NULL - Timestamp when the record was deleted
## 3. Data Types
### Text Fields
- Use `nvarchar` instead of `varchar` to support Unicode characters
- Specify appropriate length limits:
- Short names/codes: `nvarchar(60)` to `nvarchar(256)`
- Medium text: `nvarchar(512)` to `nvarchar(2000)`
- Long text/descriptions: `nvarchar(max)`
### Date and Time
- Use `datetime2(2)` for standard date/time fields
- Use `datetime2(0)` for date/time fields without milliseconds
### Boolean Values
- Use `bit` data type for boolean values
## 4. Naming Conventions
### Language Considerations
- Historical tables and columns may use German names
- All new tables and columns must use English names
- Do not rename existing German table/column names to maintain compatibility
### Table Naming
- Use PascalCase for table names
- Use plural form for entity collection tables (e.g., `Accounts`, `Employees`)
- Use singular form for lookup tables (e.g., `Status`, `Category`)
### Parent-Child Relationships
- Name child tables to reflect their relationship to the parent
- Example: `AccountDevices` and `AccountDeviceUris`
## 5. Indexing Guidelines
### Clustered Indexes
- Each table must have exactly one clustered index on the primary key (`I3D`)
### Non-Clustered Indexes
- Create non-clustered indexes on foreign key columns and frequently queried columns
- Follow naming pattern: `IX_TableName_Column1_Column2`
- Example:
```sql
CREATE NONCLUSTERED INDEX [IX_AccountDevices_AccountI3D] ON [dbo].[AccountDevices]([AccountI3D] ASC)
```
## 6. Example Tables
### Parent Table Example
```sql
CREATE TABLE [dbo].[AccountDevices](
[I3D] [int] IDENTITY(1,1) NOT NULL,
[AccountI3D] [int] NOT NULL,
[ShortName] [nvarchar](256) NOT NULL,
[DescriptionRTF] [nvarchar](max) NULL,
[Description] [nvarchar](max) NOT NULL,
[DeviceId] [nvarchar](256) NULL,
[WarrantyExpiryDate] [datetime2](0) NULL,
[CreatedDate] [datetime2](2) NOT NULL,
[CreatedByI3D] [int] NOT NULL,
[ChangedDate] [datetime2](2) NOT NULL,
[ChangedByI3D] [int] NOT NULL,
[SerialNumber] [nvarchar](60) NULL,
[Model] [nvarchar](512) NOT NULL,
[Manufacturer] [nvarchar](255) NOT NULL,
[Location] [nvarchar](255) NOT NULL,
[IsDeleted] [bit] NOT NULL,
[DeletedDate] [datetime2](2) NULL,
[DeletedByI3D] [int] NULL,
[OriginKind] [int] NOT NULL,
[BranchI3D] [int] NOT NULL,
CONSTRAINT [PK_AccountDevices] PRIMARY KEY CLUSTERED
(
[I3D] ASC
)
)
```
### Child Table Example
```sql
CREATE TABLE [dbo].[AccountDeviceUris](
[I3D] [int] IDENTITY(1,1) NOT NULL,
[AccountDeviceI3D] [int] NOT NULL,
[Kind] [int] NOT NULL,
[Uri] [nvarchar](2000) NOT NULL,
CONSTRAINT [PK_AccountDeviceUris] PRIMARY KEY CLUSTERED
(
[I3D] ASC
)
)
```
@@ -0,0 +1,65 @@
# **How to add a new right**
Creating a new right is quite simple:
First you should open `UserRightsConst.cs`, since it'll give you the infos you need. You can also open the rightsmodule in c-entron.NET, select a right, then click "I3D kopieren" in the ribbon to find the `I3D` of a specific right.
You are creating a [script](How-to-create-scripts.md) that adds a new entry to the `Sichrech` table:
- the `I3D` will later become the value of the right. The next `I3D` is on top of `UserRightsConst.cs`
- `Nummer` is not important for centron, it can be 0.
- `FomName` is important for Delphi, but not for Centron, so it can remain null.
- the same for `FomCont`.
- With `Text` the name of the right is entered as it is displayed in the rights management.
- In `OwnerRecht` the I3D of the right comes in, which is required for your new right.
- If, on the other hand, another right (or rights) requires your new right(s), `NumChildren` will show the number of them.
- at `Beschreibung`, as the name suggests, a short description comes in, which makes the right.
###ScriptHelpers.AddRightIfNotExists(..)
__It is highly advised to use `ScriptHelpers.AddRightIfNotExists(..)` and not write your own script.__
The call should look something like this.
If you need more examples check `ScriptMethod11250.cs`.
```c#
yield return ScriptHelpers.AddRightIfNotExists(
UserRightsConst.Sales.Customer.Helpdesk.SHOW_HOURLYSURCHARGERATES,
UserRightsConst.Sales.Customer.Helpdesk.ID,
"Aufschläge Stundensätze anzeigen",
"Dieses Recht gibt an, ob ein Nutzer Aufschläge Stundensätze anzeigen kann.");
```
###Raw sql script
If you have to, your script could look like this:
```xml
<Script Name="AddNewMitarbeiterauslastungRight" ScriptNumber="10870">
<UpdateQuery>
<![CDATA[
IF NOT EXISTS (SELECT * FROM Sichrech WHERE I3D = 20800042) BEGIN
INSERT INTO Sichrech
(I3D,Nummer, Text, OwnerRecht, NumChildren, Beschreibung)
VALUES
(20800042, 0, 'Mitarbeiterauslastung - nur eigene Filiale', 10930, 0, 'Die Gruppen denen dieses Recht zugewiesen wurde, können nur die eigene Filiale in der Mitarbeiterauslastung sehen.');
UPDATE Sichrech
SET NumChildren = NumChildren + 1
WHERE I3D = 10930;
END;
]]>
</UpdateQuery>
</Script>
```
At the end you write a constant for your right in `UserRightsConst` et voilà you can use your right.
@@ -0,0 +1,45 @@
# How to check user rights
Rights and groups can be managed in the `Rechteverwaltung` module.
The IDs for each user right can be found in `UserRightsConst.cs`. Always use these constants instead of the id itself.
## In viewmodels (Centron.WPF.UI)
In viewmodels it's quite easy to check for user rights:
``` csharp
CentronCache.Instance.CurrentUserAppRights.Any(f => f.I3D == UserRightsConst.T);
```
## For modules
Checking if an user has access to a module in `ModuleRegistration.cs` can be done via the first overload on:
``` csharp
ModuleRegistrationItem.For<TController>(() => Helper.HasRights(UserRightsConst.T))
```
## In BL (Centron.BL)
To check out user rights in BLs simply call `AppRightsBL.CheckRightsFromUser(currentUserI3D, your rightids)` then check if the result contains the necessary right.
``` csharp
var checkRightsResult = new AppRightsBL(Session).CheckRightsFromUser(currentUserI3D, new List<int>
{
UserRightsConst.Sales.Customer.CustomerCommon.CREATE_CUSTOMER,
UserRightsConst.Sales.Customer.CustomerCommon.EDIT_CUSTOMER,
UserRightsConst.Sales.Customer.CustomerCommon.DELETE_CUSTOMER,
UserRightsConst.Sales.Customer.CustomerCommon.SEARCH_CUSTOMER,
UserRightsConst.Sales.Customer.CustomerCommon.UNLOCK_CUSTOMER
});
```
Then check via:
``` csharp
if (checkRightsResult.Contains(UserRightsConst.Sales.Customer.CustomerCommon.CREATE_CUSTOMER) == false)
{
return Result.AsError("Fehlende Rechte um Accounts zu erstellen");
}
```
(from `AccountBL.cs`)
@@ -0,0 +1,134 @@
# How to create Mail Template
Since we are migrating all mail templates to new structure, there are few things that changed.
## New Mail Template structure
From now on, all mail templates are supposed to be in the `MailVorlagen` table, where we are saving every mail template with its own classification.
In order to accomplish that, in the class `MailTemplateReference`, you can see what defines an actual mail template (`ObjectKind`, `ObjectI3D`, `SubObjectKind`, `TemplatePrio`).
Why are these properties important? – Every type of mail-template can be uniquely identified by the combination of these values. For example Offer-Mails have ObjectKind=Offer, and all other values are NULL. If there is a MailTemplate in the database with ObjectKind=Offer, and all other values NULL, we know it has to be of type “Offer-Mail”.
ObjectI3D can be used to attach this mail-template to some other database row (for example a mail-template with ObjectKind=HelpdeskType, and ObjectI3D is that specific Helpdesk-Type it is used for).
SubObjectKind can be used as another unique identifier, if you want to group similar mail-templates together. For example, ObjectKind=Escalation has multiple mail-templates for different cases. They don’t depend on other database rows tho, so we can’t use the ObjectI3D to distinguish them. That’s where we use the SubObjectKind then.
How do we do that? – We have created a class named `MailTemplateType` which will contain the mail template skeleton definition. Here is an example how it can be added.
```csharp
public static class AccountActivities
{
public static MailTemplateReference CrmActivity => MailTemplateReference.Create(
CentronObjectKindNumeric.AccountActivity, (int)AccountActivityKind.CRM, null, defaultSubject: string.Empty, defaultBody: string.Empty);
public static MailTemplateReference Note => MailTemplateReference.Create(
CentronObjectKindNumeric.AccountActivity, (int)AccountActivityKind.Note, null, defaultSubject: string.Empty, defaultBody: string.Empty);
public static MailTemplateReference PhoneNote => MailTemplateReference.Create(
CentronObjectKindNumeric.AccountActivity, (int)AccountActivityKind.PhoneNote, null, defaultSubject: string.Empty, defaultBody: string.Empty);
}
```
Please make sure you also add default text for Subject and Body. This can be done in the same file, in class named MailTemplateDefaultText. The Body text can be added as plain text, because we are going to make sure that it’s converted to RTF in the central method.
```csharp
public class MailTemplateDefaultText
{
public static string WebOfferDefaultSubject = @"Default subject";
public static string WebOfferDefaultBody = @"Default body";
}
```
Now after this step is done, you should make sure that Mail Templates Module uses the correct Mail Template Type for the corresponding Mail Template.
This step is quite easy, but if there are questions just ask me and I will gladly help.
In the `MailTemplatsViewModel` that is in Controls, in the method `CreateMailTemplateTreeViewItemChildren` where all children are created, go to the method that is creating mail templates with the corresponding `CentronObjectKindNumeric` value.
There you must replace the parameters that are sent to `CreateMailTemplate` method with the `MailTemplateType` values for the needed parameters.
> Tip: For example, you can check how it’s done in the `CreateAccountActivitiesMailTemplates` method.
> Note: This approach will be much more simplified after we are done with the migration part, but for now we stick with it.
## Using new methods
Now that we have covered that part, it’s time to replace the way we get those mail templates and replace each call with the new method that is returning correct new mail template.
This step is also easy and very simple, but it is a bit different if we are using the logic in c-entron.NET or one of our other applications.
In `MailTemplateBL.cs` there are 2 methods. One is for the internal (in the Web-Service) usage
```csharp
public Result<MailTemplate> GetMailTemplate(MailTemplateReference mailTemplateReference, int? branchI3D)
```
and the other one is for external (SBO, Outlook AddIn, …) usage.
```csharp
public Result<MailTemplate> GetMailTemplate(CentronObjectKindNumeric objectKind, int? subObjectKind, int? objectId, int? branchI3D)
```
You probably ask yourself what the difference is and why do we have two of them. - Well, there’s no difference. The method for internal usage is just calling the method
for external usage, just because of the method parameters. Like I have already mentioned, we have this `MailTemplateType` class which is accessible only in c-entron.NET, so external
apps must pass each of the parameters, that are defining each mail template skeleton.
## Migration/Creation of the Mail Template
Now one more thing that we need to cover is migration of the existing mail templates that are saved elsewhere and creation of new mail templates.
No matter if you are creating a new template, or migrating an existing one, the process is the same.
In order to make this step easier and to avoid mistakes, we have created a new method in `ScriptHelper`. You just have to make sure to pass the scripts for getting the
correct subject and body and the other of the parameters, the rest will be handeled by the method itself.
This method will also make sure that the body is inserted as RTF.
```csharp
internal static void InsertMailTemplate(DAOSession session, string subjectScript, string bodyScript, CentronObjectKindNumeric objectKind, int? subObjectKind, int? objectI3D, int? templatePrio = null)
```
Here is one example of how the migration can be done.
```csharp
public override Result ExecuteScript(DAOSession session)
{
ScriptHelpers.InsertMailTemplate(session,
GenerateAppSettingsSQL(AppSettingsConst.HelpdeskForwardingInternalEmailSubject),
GenerateAppSettingsSQL(AppSettingsConst.HelpdeskForwardingInternalEmailBody),
CentronObjectKindNumeric.HelpdeskClass,
1,
null);
ScriptHelpers.InsertMailTemplate(session,
GenerateAppSettingsSQL(AppSettingsConst.HelpdeskForwardingExternalEmailSubject),
GenerateAppSettingsSQL(AppSettingsConst.HelpdeskForwardingExternalEmailBody),
CentronObjectKindNumeric.HelpdeskClass,
2,
null);
}
private string GenerateAppSettingsSQL(AppSettingsConst settingsConst)
{
return $@"SELECT WertMemo FROM Stammdat WHERE I3D = {(int)settingsConst}";
}
private string GenerateTextModuleSQL(TextModuleType type)
{
return $@"SELECT Text FROM Textbaustein WHERE Art = {(int)type} AND SichbenuI3D = 0 AND KundenI3D = 0";
}
```
> :exclamation: Please, make sure that this step is required only if the mail template is not in the MailVorlagen table.
> Tip: In the `MailTemplateBL.cs` there is still a method `GetAllDelphiMailTemplates`, which can help you to get the needed parameters info faster as well.
@@ -0,0 +1,339 @@
# How to EndToEnd test
## General
EndToEnd tests are supposed to test everything from the `WebServiceBL` layer and downwards (see the diagram [here](GeneralStructure.md)).
In other words, with EndToEnd tests we are testing the `WebServiceBL`, the `BL` and the `database`.
The project can be found at `/tests/Centron.Tests.EndToEnd` and all the tests are supposed to be in the `Tests` namespace.
For example, there are tests for the c-entron.NET `Receipt Area` are at `/tests/Centron.Tests.EndToEnd/Tests/Receipts/ReceiptTest.cs`.
The tests are executed as part of every Pull-Request.
If one of the tests fails, the PR cannot be merged.
> Caution!
> Some tests are a bit flaky sometimes. That means they fail even tho they should not.
> In these cases you can just let the PR get built again, and the tests should run successful.
We should try to make all tests NOT flaky.
## Different kinds of tests
We can roughly categorize all tests into 2 categories:
1. [General purpose tests](#general-purpose-tests)
2. [Specialized tests](#specialized-tests)
They are only different in what they do, and it's not a hard categorization either.
You still create both kinds of tests the same way, and they will look the same.
They both inherit from `EndToEndTest.cs` and only differ in **what** and **how much** they do.
You can also inherit from `CentronTest.cs`, if you don't need database access for your test.
And also, there is a `PerformanceTest.cs`, that measures the performance of some code, and fails if the performance-goal is not met.
We can use this kind of test to make sure, that we don't have too harsh performance-regressions when we change some things.
For example, we have a test that makes sure big receipts can be saved in less than 5 seconds.
If we add something to the SaveReceipt method, or change some other things around, and the receipt suddenly takes 8 seconds to save, then we made a mistake and have to correct it.
### General purpose tests
These tests are general purpose, doing every day things that the user does too.
For example `creating a new customer`, `adding addresses` and `contact-persons` to it, `renaming contact-persons`, `removing addresses` and so on.
The general purpose tests are supposed to cover **really much code**.
We want them to fail if we break a workflow that the user is doing every day.
### Specialized tests
The specialized tests are way smaller than the general purpose tests and are usually created as a response to a ticket that was fixed, or a new feature that was created.
For example `make sure that a setting works as expected`, or `make sure that a bug was fixed`.
These specialized tests are supposed to cover **really small amounts of code**, just enough to make sure the specific thing works as expected.
We want them to fail if we break a specialized setting that some users might be using.
## How does it work?
1. Before each test is executed a database backup (`/Infrastructure/DatabaseBackup.zip`) is restored
2. The `DAOFactory` is configured to use that new database
3. Any new scripts (`IScriptMethod`) are executed
4. The `Test.Execute` method is executed
5. The database backup is removed
This workflow means that every test always has the same starting configuration.
This fact is extremely useful because it allows us to execute these tests today, or in a month, or in a year, and we will still get the same result in the end.
If you create new entries in a table for example, they will get the same I3D every time.
Actually, that's the most important thing about these tests, and why they even work at all: **The tests are repeatable!**
If they were not repeatable, they would fail without any real breaking changes, and then they are not useful to find the breaking changes.
## How to run the tests on your machine
Because we restore the database backup before each test, and the backup is on your local machine, the **SQL Server also needs to be on your local machine**.
So if I work on my own machine `CS-ULM-DANHAE`, and I want to run some tests, I absolutely need a SQL Server installed on my own machine (`CS-ULM-DANHAE\SQL2017` in my case).
**You cannot use a SQL Server on a different machine.**
> You need at least an SQL Server 2017.
To **configure the connection to the SQL Server** you have to **change some small things** in the `/Infrastructure/Database.cs` class.
You will see three properties at the top: `Server`, `Username` and `Password`. They all look similar to this:
``` csharp
public static string Server
{
get
{
var server = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_SERVER");
if (string.IsNullOrWhiteSpace(server) == false)
return server;
throw new Exception("You have to configure the SQL server for EndToEndTests");
return "YOUR-MACHINE\\SQL2017"; //Enter your local SQL server here
}
}
```
You can see, it at first tries to get the configured value from an **environment variable** - that is exactly how the code gets the connection when it's run by the **build server**.
The build-server will set the environment variables to a SQL Server installed on the build-server itself (so it's locally to where the tests will be executed).
But if the environment variable is not set, it will **throw an Exception**.
If you want to configure the EndToEnd tests to use your own local SQL Server, you just have to **comment out the exception**, and **change the return value of the property**.
```csharp
public static string Server
{
get
{
var server = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_SERVER");
if (string.IsNullOrWhiteSpace(server) == false)
return server;
//throw new Exception("You have to configure the SQL server for EndToEndTests");
return "CS-ULM-DANHAE\\SQL2017"; //Enter your local SQL server here
}
}
```
If you do the same thing for the `Username` and `Password` properties, you are done and can execute the tests locally on your machine.
> :exclamation: Make sure to not commit these changes! :exclamation:
## How data is validated
When executing the test, you will probably create some data or some objects.
For example, you will create a new customer, a new ticket, or a new receipt.
The test has to validate, that these objects look exactly how expected.
This is done with a class called `CentronVerifier`. It has a `Verify` method that takes the data you want to verify, and a name for it.
```csharp
this.Verifier.Verify("CreatedOffer", offer);
```
This will create a file called `CreatedOffer.actual.txt` that is basically the `offer` json serialized.
It will then compare this file, to a `CreatedOffer.expected.txt` file.
If they **differ**, **something has changed**, and the **test fails**.
If they are **the same**, **everything still works** as expected, and the **test continues**.
The first time you execute a test, of course there will be no `*.expected.txt` file, and thus the test will fail.
You're supposed to go ahead, and rename the `*.actual.txt` one to `*.expected.txt`.
So when you execute the test again, a new `*.actual.txt` file will be created, and compared to the `*.expected.txt`.
This also means, the `*.expected.txt` should be added to source-control, but the `*.actual.txt` files should **NOT** be added to source-control.
You can also verify data from a table by using the `VerifyTable` method.
```csharp
this.Verifier.VerifyTable("TheCustomer", "dbo.Kunden", where: "I3D = 12345");
```
With that you have to provide a name again, the table that you want to verify, and a optional where filter.
A `SELECT * FROM [Table] WHERE [Filter]` statement will be executed, and the result will be used as data for the `Verify` method.
So everything with the `*.expected.txt` and `*.actual.txt` files works exactly the same as with the `Verify` method.
## Tips and other useful things
### Run any sql statement in your test
The base class for all tests `EndToEndTest.cs` provides a useful method `Sql` to run any sql statement in your test.
You can use it for example if you easily wanna change a setting in the database before running your test. Or you want to change a configuration property on a customer or something else.
```csharp
this.Sql("UPDATE Stammdat SET Wert = 1 WHERE I3D = 1234");
```
### Debugging a test
You can also very easily debug a test by setting breakpoints in your code, and then `Right-Click` -> `Debug` the test in the `Test Explorer`.
![Debug a test](./.attachments/How-to-end-to-end-test/DebugTest.png)
### Diff view for *.expected.txt and *.actual.txt
If a test fails because something has changed between a `*.expected.txt` and a `*.actual.txt` file, you can see a diff between those two files.
This allows you to more easily spot the differences.
It is pretty easy to do. If a test fails because of a `this.Verifier.Verify` call, you just have to [debug the test](#debugging-a-test).
Now if the `Verify` call fails the test will fail as usual, but at the same time a Diff-View will open in Visual Studio with both the `*.expected.txt` and the `*.actual.txt` in it.
You can then more easily decide whether the changes are fine, or whether you broke something.
## How to create a test
Lets say we want to write a [general purpose test](#general-purpose-tests) for some `Helpdesk` stuff - creating a new ticket for a customer, setting some values and saving it.
> The real general purpose HelpdeskTest is of course supposed to cover even more of the helpdesk code.
> For example forwarding it, adding documents and history entries, and closing the ticket.
### Step 1
Make sure to [configure the database connection](#How-to-run-the-tests-on-your-machine) so you can run them on your computer.
### Step 2
Create a new namespace/folder under the `/Tests` namespace: `WikiHelpdesk`.
### Step 3
In that namespace, create a new class `WikiHelpdeskTests.cs`.
### Step 4
Make that class inherit from `EndToEndTest`.
### Step 5
Implement the constructor for the class, and override the `Execute` method.
Your class should look like this now:
```csharp
using Centron.Tests.EndToEnd.Infrastructure;
using Xunit.Abstractions;
namespace Centron.Tests.EndToEnd.Tests.WikiHelpdesks
{
public class WikiHelpdeskTests : EndToEndTest
{
public WikiHelpdeskTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
}
public override void Execute()
{
}
}
}
```
> This is the basic setup you always have to do for a test.
> Everything that follows now is actually implementing the test and [validating data](#how-data-is-validated).
### Step 6
Create a new method `CreateTicket`
```csharp
private HelpdeskDTO CreateTicket()
{
using (var session = new BLSession())
{
//Get a LoggedInUser for the current user using the base class method
var loggedInUser = this.GetLoggedInUser(11); // Default is 11 if no parameter is provided
//Create a new ticket
var ticket = session.GetBL<HelpdeskWebServiceBL>().GetNewTicketFromCustomer(loggedInUser.User, 10022);
//Set some values
ticket.ShortDescription = "Mein super wichtiges Ticket!";
ticket.Description = "Mit einer nicht sehr ausführlichen Beschreibung.";
//Save the helpdesk
var saveHelpdeskResult = session.GetBL<HelpdeskWebServiceBL>().SaveHelpdesk(ticket, loggedInUser);
//Verify the saveHelpdeskResult
this.Verifier.Verify("SaveHelpdeskResult", saveHelpdeskResult);
//Load the complete helpdesk object
var savedTicket = session.GetBL<HelpdeskWebServiceBL>().GetHelpdeskByI3D(saveHelpdeskResult.Data, loggedInUser);
//Verify the savedTicket
this.Verifier.Verify("SavedTicket", savedTicket);
return savedTicket.Data;
}
}
```
I wanna especially highlight the two lines that use the `this.Verifier`:
```csharp
this.Verifier.Verify("SaveHelpdeskResult", saveHelpdeskResult);
```
and
```csharp
this.Verifier.Verify("SavedTicket", savedTicket);
```
These calls make sure that the [data is actually validated](#how-data-is-validated).
### Step 7
Call the new `CreateTicket` method in the `Execute` method.
```csharp
public override void Execute()
{
var ticket = this.CreateTicket();
}
```
You created your first test! :tada:
### Step 8
As you know from the chapter on [how data is actually validated](#how-data-is-validated) the test will fail right now.
There is no `SaveHelpdeskResult.expected.txt` and no `SavedTicket.expected.txt`.
So lets run the test by opening the `Test Explorer` in Visual Studio and `Right-Click` -> `Run` the test.
![Run the test](./.attachments/How-to-end-to-end-test/RunTest.png)
It will take a little while, but after like 40 seconds you should get a failed test result.
![Failed test run](./.attachments/How-to-end-to-end-test/FailedTest.png)
### Step 9
If you activate `Show All Files` in the solution explorer for the `Centron.Tests.EndToEnd` project, you should see the `SaveHelpdeskResult.actual.txt` file.
![SaveHelpdeskResult.actual.txt file in solution explorer](./.attachments/How-to-end-to-end-test/SaveHelpdeskResult-actual-txt.png)
Go ahead and rename it to `SaveHelpdeskResult.expected.txt`.
It automatically gets picked up by the csproj file too.
![SaveHelpdeskResult.expected.txt file in solution explorer](./.attachments/How-to-end-to-end-test/SaveHelpdeskResult-expected-txt.png)
### Step 10
Run the test again!
...
As expected, it still fails because of the second `Verify` call.
But you now know what to do.
### Step 11
A new file `SavedTicket.actual.txt` was created.
Feel free to open it, and look at the data that is verified.
Every single line in that JSON document has to stay exactly the same.
Every property in that file is not allowed to change.
![SavedTicket.actual.txt file in solution explorer](./.attachments/How-to-end-to-end-test/SavedTicket-actual-txt.png)
Rename it to `SavedTicket.expected.txt`.
![SavedTicket.expected.txt file in solution explorer](./.attachments/How-to-end-to-end-test/SavedTicket-expected-txt.png)
### Step 12
Run the test once again!
...
IT WORKS! :tada:
![Successful test](./.attachments/How-to-end-to-end-test/SuccessfulTest.png)
The test is finished!
You can now add even more logic to the `Execute` method.
@@ -0,0 +1,15 @@
# Fixed Memory leaks
A collection of memory leaks we fixed and where they were.
# 1. DependencyPropertyDescriptor.FromProperty
Previously found in "ContentControlContentChangedBehavior".
Switching to PropertyChangeNotifier which just creates a normal Binding fixes the issue.
# 2. DispatcherTimer
Previously found in "TicketDetailView.xaml.cs"
A DispatcherTimer started with '.Start()' need a '.Stop()' call.
If you use '+=' u likewise need a '-='.
@@ -0,0 +1,183 @@
# Settings Management in c-entron.NET
This guide explains how application settings are managed in the c-entron.NET project, covering both legacy and current approaches, and providing best practices for working with settings.
## Overview
The c-entron.NET application uses two separate database tables for storing application settings:
1. **Stammdat** - Legacy settings table with historical settings
2. **ApplicationSettings** - Current table for new settings
This dual-table approach exists for historical reasons, and all new settings should be added to the `ApplicationSettings` table.
## Settings Tables
### Legacy: Stammdat Table
The Stammdat table contains many historical settings that are accessed through the `AppSettingsConst` enum.
- **Enum File**: `src/backend/Centron.BL/Administration/Settings/AppSettingsConst.cs`
- **Access**: Settings are accessed through the `AppSettingsBL.GetSettings(AppSettingsConst)` method
- **Updates**: Although we maintain these settings, no new settings should be added to this table
### Current: ApplicationSettings Table
The ApplicationSettings table is the current standard for all new application settings.
- **Enum File**: `src/backend/Centron.Interfaces/Administration/Settings/ApplicationSettingID.cs`
- **Next Free ID**: Tracked in a comment at line 15 of `ApplicationSettingID.cs`
- **Access**: Settings are accessed through the `AppSettingsBL.GetSettings(ApplicationSettingID)` method
- **Settings Descriptions**: Defined in `src/backend/Centron.Interfaces/Administration/Settings/ApplicationSettingDefinitions.cs`
## ID Management
When adding new settings, you must:
1. Check the next available ID from the comment in `ApplicationSettingID.cs`:
```csharp
// Next Centron Settings ID : 10370
// Current Riverbird Settings ID : 50035
```
2. Use the next ID in sequence (in this case, 10370)
3. Update the comment with the next available ID after adding your setting
The Riverbird setting IDs (starting with 50xxx) are not used by c-entron.NET.
## Setting Definitions
For each new setting in `ApplicationSettingID.cs`, you must add a corresponding description in the `ApplicationSettingDefinitions.cs` file:
```csharp
case ApplicationSettingID.YourNewSetting:
return "Description of what this setting does and how it's used.";
```
The description should clearly explain:
- The purpose of the setting
- What data it stores
- Any relevant format or validation constraints
## Accessing Settings
### Group Setting Classes
The client never accesses settings tables directly. Instead, we use "group setting classes" to manage related settings. These classes:
1. Load settings from the database
2. Provide a strongly-typed interface for accessing settings
3. Manage updating settings back to the database
### Example: Loading Settings
```csharp
// Example from ReceiptWebServiceBL.GetReceiptInvoiceSettings()
var appSettings = this._appSettingsBL.GetSettings
(
ApplicationSettingID.InvoiceArchiveActive,
ApplicationSettingID.IsZugferdInvoiceActive,
// additional settings...
);
var settings = new ReceiptInvoiceSettingsDTO
{
IsInvoiceArchiveActive = appSettings.GetBool(ApplicationSettingID.InvoiceArchiveActive, false),
IsZugferdInvoiceActive = appSettings.GetBool(ApplicationSettingID.IsZugferdInvoiceActive, false),
// map other settings...
};
return Result<ReceiptInvoiceSettingsDTO>.AsSuccess(settings);
```
### Example: Saving Settings
```csharp
// Example from ReceiptWebServiceBL.SaveReceiptInvoiceSettings()
var updateSettings = this._appSettingsBL.GetSettingsForUpdate
(
ApplicationSettingID.InvoiceArchiveActive,
ApplicationSettingID.IsZugferdInvoiceActive,
// additional settings...
);
// Update values
updateSettings.UpdateBool(ApplicationSettingID.InvoiceArchiveActive, settings.IsInvoiceArchiveActive.Value);
updateSettings.UpdateBool(ApplicationSettingID.IsZugferdInvoiceActive, settings.IsZugferdInvoiceActive.Value);
// update other settings...
// Save all changes
updateSettings.SaveSettings();
return Result<bool>.AsSuccess(true);
```
## API Integration
Settings are exposed through API methods, allowing client applications to retrieve and update settings.
### API Patterns
1. All setting API methods must use HTTP POST
2. Get methods return a DTO containing the settings
3. Save methods accept a DTO with the settings to update
### Example API Methods
```csharp
// In ICentronRestService.cs
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json)]
Result<ReceiptInvoiceSettingsDTO> GetReceiptInvoiceSettings();
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json)]
Result<bool> SaveReceiptInvoiceSettings(ReceiptInvoiceSettingsDTO settings);
```
## Best Practices
### Adding New Settings
1. Use the next available ID from the comment in `ApplicationSettingID.cs`
2. Add your setting to the `ApplicationSettingID` enum
3. Update the "Next Centron Settings ID" comment
4. Add a detailed description in `ApplicationSettingDefinitions.cs`
5. Create or update group setting classes to access your setting
### Setting Types
Application settings support multiple data types:
- **Boolean**: Use `GetBool()` / `UpdateBool()` methods
- **Integer**: Use `GetInt()` / `UpdateInt()` methods
- **String**: Use `GetString()` / `UpdateString()` methods
- **Large String**: Use `GetLargeString()` / `UpdateLargeString()` methods
- **Enum**: Use `GetEnum<T>()` / `UpdateEnum<T>()` methods
- **Decimal**: Use `GetDecimal()` / `UpdateDecimal()` methods
### Default Values
When retrieving settings, always provide a default value in case the setting doesn't exist:
```csharp
// Example with default value
bool isActive = appSettings.GetBool(ApplicationSettingID.SomeSetting, false);
```
## Common Scenarios
### Creating a New Group Settings Class
1. Define a DTO class to hold the settings
2. Create Get method that loads settings from AppSettingsBL
3. Create Save method that updates settings with AppSettingsBL
4. Add corresponding API methods
### Migrating Legacy Settings
When migrating settings from Stammdat to ApplicationSettings:
1. Add the new setting to ApplicationSettingID
2. Add its description to ApplicationSettingDefinitions
3. Update code to read from both sources during transition
4. Eventually remove the old setting access after migration
@@ -0,0 +1,15 @@
#A couple of Links useful for XRechnung
Offical Documentation PDF is embedded in the source code (ZUGFeRD-2.1.1 - Spezifikation_TA (1)).
Here the link for this document in the official Version archive: https://www.ferd-net.de/standards/zugferd-version-archive/zugferd-2.1.html
##Opensource Implementation
https://github.com/stephanstapel/ZUGFeRD-csharp
Nice place to check out some Stuff, has a couple of Demo files.
Would be nice to switch to this in the future instead of creating our own implementation.
##KOSIT (Koordinierungsstelle für IT-Standards)
https://github.com/itplr-kosit
Different helpful repositories for XRechnung.
For example a cli tool to verify the xml.
@@ -0,0 +1,241 @@
# How to add webservice methods
Please refer to the [General Structure](../../getting-started/general-structure.md) page to get an overview of the general structure.
Please refer to the [On DTOs and Entities](../../reference/architecture/dtos-and-entities.md) to get a better understanding on what types to use where.
For a reference with a correct implementation check out `MyDayBL.cs` and associated classes.
## Businesslogic
### BL
For this we will assume that `ThingyBL.cs` contains the method `public Result<Thingy> SaveOrUpdateThingy(Thingy item, LoggedInUser currentUser)`. This method checks the input, checks userrights and saves the `Thingy` to the database.
BL methods should always return a result with actual useable results (don't just `return Result.AsSuccess()`). Save methods must always return the entity itself so we get an entity with a valid I3D.
### WebServiceBL
The webserviceBL must be named the same as the BL (`ThingyWebServiceBL.cs` in this case) and the method itself should be named the same as the BL one (`public Result<ThingyDTO> SaveOrUpdateThingy(ThingyDTO item, LoggedInUser currentUser)` in our example).
It's sole purpose is to convert and incoming `ThingyDTO` to a `Thingy` entity and back. Please consult [On DTOs and Entities](../../reference/architecture/dtos-and-entities.md) on how to convert correctly.
This must also return a `Result<Thingy>`. The best way to ensure the result from the BL gets carried over is using `Result<Thingy>.FromResult(<your ThingyDTO>, <yourBLResult>);`.
Our method should look something like this:
``` csharp
public Result<ThingyDTO> SaveOrUpdateThingy(ThingyDTO item, LoggedInUser currentUser)
{
var entity = this.ConvertThingyDTOToEntity(item);
var result = this._thingyBL.SaveOrUpdate(entity, currentUser);
var mappedResult = this.ConvertThingyToDTO(result.Data);
return Result<ThingyDTO>.FromResult(mappedResult, result);
}
```
## RestService
Next we'll create the method in the actual webservice. Please note that both `CentronRestService` and `ICentronRestService` are partial classes and have parts for some categories (`CentronRestService.Helpdesk`, `CentronRestService.Docuboard` etc.). Also note that the methods here are wildly inconistently placed and are pretty much all over the place.
If you find a partial part for the method you want to create use that part, if you know you'll create more than 2-3 methods consider making your own partial part and if neither of those just throw it into `CentronRestService` (or `CentronRestService.DTOPart`).
As our API has a flat surface methods here need to be named more specific than they are in the BL-layers. They **must** contain the name of your entity or something else that make them identifiable on what category they belong in. For example we work with `Thingy` here, so all webservice methods doing anything with them (load/saveorupdate/delete etc.) have to include `Thingy` somewhere (LoadThingies, SaveOrUpdateThingy etc.).
### CentronRestService
It **must** always accept a `Request<T>`/`Request` and return a `Response<T>`/`Response` and nothing else.
The method itself can only contain 2 rows:
1. call the webserviceBL
2. convert the `Result<T>` to a `Response<T>` and return
This is how our method should look like:
``` csharp
public Response<ThingyDTO> SaveOrUpdateThingy(Request<ThingyDTO> request)
{
var result = this.Session.GetBL<ThingyWebServiceBL>().SaveOrUpdateThingy(request.Data, this.GetLoggedInUserByTicket(request.Ticket));
return Response<ThingyDTO>.FromBLResult(result);
}
```
Note the `GetLoggedInUserByTicket(request.Ticket)`-Method to get a `LoggedInUser`.
### ICentronRestService
Creating the method in the interface is very simple, just copy the method signature from the CentronRestService-method over, then add the needed attributes. We always need the following 3 attributes to register the method for the webservice. Please note that the [WebInvoke..] method **must** be 'POST' and the UriTemplate **must** be the same as the method name.
``` csharp
[WebInvoke(Method = "POST", UriTemplate = "SaveOrUpdateThingy")]
[Authenticate]
Response<ThingyDTO> SaveOrUpdateThingy(Request<ThingyDTO> request)
```
With this we have a working webservice method that can be used by external apps via the `Centron.WebServices.Core` / `Centron.Interfaces` reference or a WSDL-Service-Reference or similar.
*Remember to connect to your self-hosted webservice when creating methods or you'll be very confused when it doesnt work (speaking from experience ;) )*
### Request classes
Quite often you need to send several properties in your request for example in filters. For this simply create a `<yourWebServiceMethodName>Request` that aggragates all the needed properties and then use `Request<YourRequest>` as our parameter in the restservice. So if we wanted to filter our `Thingies` we would write our request like this:
``` csharp
[DataContract]
public class GetThingiesRequest
{
[DataMember]
public List<int> ThingyI3Ds { get; set; }
[DataMember]
public DateTime? Before { get; set; }
[DataMember]
public DateTime? After { get; set; }
}
```
``` csharp
[WebInvoke(Method = "POST", UriTemplate = "GetThingies")]
[Authenticate]
Response<ThingyDTO> GetThingies(Request<GetThingiesRequest> request)
```
Note, that these requests are basically DTOs and all restrications/problems of DTOs apply.
Request go into the `Centron.WebServices.Core` project and then the `RestRequests` directory.
## Logics
To use the webservice method in c-entron<span>.NET we must also implement the ILogic interfaces.
Again search for an existing fitting `ILogic` for your method or create your own (`IThingyLogic` for us).
### ILogic
Methods in your ILogic **must** return a `Task<Result<T>>` and should be named the same as the webservice method. But here we can be unspecific again as the context comes from the Logic itself, so instead of `SaveOrUpdateThingy` we can just use `SaveOrUpdate` if we want.
``` csharp
public interface IThingyLogic
{
Task<Result<ThingyDTO>> SaveOrUpdateThingy(ThingyDTO item);
}
```
### BLLogic
This is necessary for the direct database connection and must be named `BL..Logic` (`BLThingyLogic` for us).
It must have 1 constructur that accepts a `ConnectionInfo`:
``` csharp
private readonly ConnectionInfo _connectionInfo;
public BLThingyLogic(ConnectionInfo connectionInfo)
{
Guard.NotNull(connectionInfo, nameof(connectionInfo))
this._connectionInfo = connectionInfo;
}
```
The method itself should only start a task via `Task.Run(..)`, in this task create the webserviceBL and call the correct method. It should look something like this:
``` csharp
public Task<Result<ThingyDTO>> SaveOrUpdateThingy(ThingyDTO item)
{
return Task.Run(() =>
{
using (var session = new BLSession())
{
return session.GetBL<MyThingyWebServiceBL>().SaveOrUpdate(item);
}
});
}
```
### WSLogic
This is necessary for the webservice connection and must be named `WS..Logic` (`WSThingyLogic` for us).
It must have 1 constructur that accepts a `CentronWebServiceConnection`:
``` csharp
private readonly CentronWebServiceConnection _webServiceConnection;
public WSThingyLogic(CentronWebServiceConnection webServiceConnection)
{
Guard.NotNull(webServiceConnection, nameof(webServiceConnection))
this._webServiceConnection = webServiceConnection;
}
```
The method itself simply uses an expression to call the ICentronRestService method you just created.
It is important to never create a request yourself, instead always create it via _webServiceConnection, as this ensures the ticket is set properly.
If you don't need to await the call:
``` csharp
public Task<Result<ThingyDTO>> SaveOrUpdateThingy(ThingyDTO item)
{
return this._webServiceConnection.CallWebServiceMethodWithSingleResultAsync(f => f.SaveOrUpdateThingy(this._webServiceConnection.GetRequest(item)));
}
```
or, if you need to actually await it:
``` csharp
public async Task<Result<ThingyDTO>> SaveOrUpdateThingy(ThingyDTO item)
{
var result = await this._webServiceConnection.CallWebServiceMethodWithSingleResultAsync(f => f.SaveOrUpdateThingy(this._webServiceConnection.GetRequest(item))).ConfigureAwait(false);
// your logic
return result;
}
```
If you await the call, you also need to add the `.ConfigureAwait(false)`. Always try to return the task instead of awaiting it.
The webservice can be called with several methods depending the signature of your webservice method:
returntype | method to use
---|---
Result|CallWebServiceMethodWithResultAsync\<TRequest>
Result\<T>|CallWebServiceMethodWithSingleResultAsync<TRequest, TResult>
Result\<IList\<T>>|CallWebServiceMethodWithListResultAsync<TRequest, TResult>
`TRequest` must be equal to `T` in your `Request<T>` in `ICentronRestService`. As such there can never be a `TRequest` of type `Request<T>`/`Request`.
These methods can also be used without request objects (when your `ICentronRestService` methods parameter is a simple `Request`):
returntype | method to use
---|---
Result|CallWebServiceMethodWithResultAsync
Result\<T>|CallWebServiceMethodWithSingleResultAsync\<TResult>
Result\<IList\<T>>|CallWebServiceMethodWithListResultAsync\<TResult>
## How to use in c-entron<span>.NET
There are two ways to call a webservice method from a viewmodel in Centron.WPF.UI.
If you need the Logic just once simply use `ClassContainer.Instance.WithInstance(..)` like this:
``` csharp
var thingy = await ClassContainer.Instance.WithInstance((IThingyLogic logic) => logic.GetThingyByI3D(thingyI3D)).ThrowIfError();
```
If you need the logic several times, you should create it once via `ClassContainer.Instance.GetInstance<T>()` and use that in your class. This means you also need to implement the `IDisposable` interface and release the instance upon disposing by calling `ClassContainer.Instance.ReleasInstance(yourInstance)`.
``` csharp
public class ThingiesViewModel : IDisposable
{
..
public readonly IThingyLogic ThingyLogic;
public ThingiesViewModel()
{
this.ThingyLogic = ClassContainer.Instance.GetInstance<IThingyLogic>();
}
public void Dispose()
{
ClassContainer.Instance.ReleaseInstance(this.ThingyLogic);
}
..
public void Get()
{
var thingy = await this.ThingyLogic.GetThingyByI3D(thingyI3D).ThrowIfError();
..
}
}
```
Please note the use of `await` and `ThrowIfError()` in both cases.
@@ -0,0 +1,127 @@
# Run the c-entron Web-Service on Linux
Right now we don't create Linux version of our web-service by default.
But you can create a version manually by running the `build-web-service-linux` target from the `Centron.Scripts` project.
It creates a `c-entron Web-Service Linux.zip` file which contains the web-service for Linux.
# How to prepare the Linux server
The Linux web-service is a framework-dependent version, which means you need to install .NET on the Linux server.
[How to install .NET on Linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
The above link also contains a list of all Linux distributions that are supported by .NET.
As far as I know, we should support all of them, but we only tested with Ubuntu and Debian.
Make sure to install the correct version of .NET that is needed for our web-service.
As of right now (Jan. 2024), c-entron.NET and the web-service are running on .NET 8.
Also some other dependencies are required for the web-service to work correctly.
[See here which other dependencies are required](https://docs.devexpress.com/OfficeFileAPI/401441/use-office-file-api-on-linux#prerequisites)
# How to install the c-entron Web-Service on Linux
Copy the `c-entron Web-Service Linux.zip` archive to the server, and extract it wherever you want to install it.
For example purposes, lets use `/opt/centronws`.
Now we need a `WebServiceConfig.xml`, which on windows is created by the `c-entron Web-Service Connection Manager` - but this tool is not available on linux, and right now we don't have a linux-alternative for it.
So I recommend to copy the `WebServiceConfig.xml` from a windows-installation of the web-service.
After the web-service is copied and configured, you can start it by executing the `Centron.Host.Console` application.
You might get some permission errors along the way - fix them by making the `Centron.Host.Console` executable, and giving read-permissions to the directory.
You should see some command-line output from the web-service, and after a couple of seconds the web-service should be started and available per HTTP.
# How to use HTTPS
HTTPS configuration is very different between Linux and Windows, because on Windows its using Windows-only configuration.
So on Linux, you have to edit the `WebServiceConfig.xml`.
Add a `WebServiceCertificateFilePath` XML-node with the path to your pfx-certificate.
And add another XML-node `WebServiceCertificatePassword` containing your password for the pfx-certificate.
For example, if you placed the certificate next to the web-service `/opt/centronws/https-certificate.pfx`, and the password is `123456`, adjust the `WebServiceConfig.xml` to look like this.
```xml
<WebServiceConfig>
<!-- Usual settings that also work for windows -->
<WebServiceAddress>https://localhost:443</WebServiceAddress>
<DatabaseConnectionString>VGhpcyBpcyB3aGVyZSB5b3VyIERhdGFiYXNlQ29ubmVjdGlvblN0cmluZyBzaG91bGQgYmU=</DatabaseConnectionString>
<!-- The 2 following nodes should be added manually -->
<WebServiceCertificateFilePath>./https-certificate.pfx</WebServiceCertificateFilePath>
<WebServiceCertificatePassword>123456</WebServiceCertificatePassword>
<!-- Other settings omitted -->
</WebServiceConfig>
```
Restart the `Centron.Host.Console`, and the c-entron Web-Service should be available per HTTPS with a valid certificate!
# How to make the web-service run in the background
You can use whatever mechanism or service you want for that, just execute the `Centron.Host.Console` application.
One possibility is, to use `systemd` to run the web-service (this obviously only works if `systemd` is available on your Linux distribution).
To use `systemd` create a new file `/etc/systemd/system/centronws.service` with the following contents (adjust paths and user if necessary):
```ini
[Unit]
Description=c-entron web-service
[Service]
WorkingDirectory=/opt/centronws # will set the Current Working Directory (CWD)
ExecStart=/opt/centronws/Centron.Host.Console # systemd will run this executable to start the service
SyslogIdentifier=centronws # to query logs using journalctl
User=c-entron # which user should execute the service, use 'chown yourusername -R /opt/centronws' to take ownership of the folder and files, use 'chmod +x /opt/centronws/Centron.Host.Console' to allow execution of the executable file.
Restart=always # ensure the service restarts after crashing
RestartSec=5 # amount of time to wait before restarting the service
KillSignal=SIGINT # copied from dotnet documentation at https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-3.1#code-try-7
AmbientCapabilities=CAP_NET_BIND_SERVICE # give the executed process the CAP_NET_BIND_SERVICE capability. This capability allows the process to bind to well known ports.
[Install]
WantedBy=multi-user.target
```
After creating this `.service` file, we have to execute some commands.
Reload the systemd services using `sudo systemctl daemon-reload`
Make sure the new service was discovered correctly using `sudo systemctl status centronws`
Start the c-entron Web-Service using `sudo systemctl start centronws.service`
If you want the c-entron Web-Service to start when the machine starts, you can run `sudo systemctl enable centronws.service`
# Differences between Linux and Windows as a c-entron User
There are a couple of limitations when running on Linux, in comparison to running on Windows.
Most of them can be fixed in the future, but right now these limitations apply.
### Sub-Web-Services
The Sub-Web-Services don't work on Linux. All of the code regarding them is contained in the `c-entron Web-Service Connection Manager`, which is not available on Linux.
A refactoring is needed here.
**Our latest stance on this topic is:**
If a customer is running his web-service on linux, then he is a `poweruser`.
And as a `poweruser` he can copy the web-service, configure it for another database, and create another `systemd service` all by himself.
# What is left to do to make the Linux UX good?
### Web-Service configuration tool
On windows we have the `Web-Service Connection Manager`, but we currently don't have a similar tool for linux.
Right now you're out of luck and have to hand-edit the `WebServiceConfig.xml` file, which is a very bad user experience.
What I would like to have is some kind of CLI that guides the user through web-service configuration.
It would be extra nice if you could call the `Centron.Host.Console.exe` with a `configure` command, and then it steps you through the configuration (web-service url, certificate, path, database connection, etc.).
### Certificate password encryption
Right now the certificate-password is written in `plaintext` in the `WebServiceConfig.xml` file.
Once we have the `Web-Service configuration tool` from above, we should `encrypt` the certificate-password instead.
Just as we already encrypt the `database connection-string` and `proxy password`.
### Tool to read your Hardware-ID
On windows it's very easy, because the `Web-Service Connection Manager` shows you your `Hardware-ID`.
On linux this is currently not available, and you as the user can't really do anything by yourself.
Right now the workaround is: Send your linux `/etc/machine-id` to a developer, and he magically creates the `Hardware-ID` from it.
A better experience would be, to have some kind of CLI that prints you your own `Hardware-ID`.
Would be nice if it also was on `Centron.Host.Console.exe`, with a `hardware-id` command, and all it does is print out your `Hardware-ID`.
@@ -0,0 +1,3 @@
# How to create a dialog window
I don't know, but I would like to - please tell me.
@@ -0,0 +1,116 @@
# How to create a module in c-entron.NET
A module in the c-entron<span>.NET is just a view that can be displayed in the main window. It can show up in the module list on the left and can be docked and undocked from the main window.
We'll be using a module to manage our `Thingies` again. If you want to know how to create webservice methods for our thingies check out [this](How-to-add-webservice-methods.md).
## ICentronAppModuleController
The first class to create is a `..AppModuleController`. For us this will be named `ThingiesAppModuleController`.
This class needs to implement the `ICentronAppModuleController` interface. If you go to the `ICentronAppModuleController.cs` file there are summaries for each property and how to fill them properly.
``` csharp
public class TicketListAppModuleController : ICentronAppModuleController
{
public string ModuleName => "Dinge-Liste";
public BitmapImage ImageSmall => new BitmapImage(new Uri("pack://application:,,,/c-entron 2.0;component/Images/Icons/16x16/Thingies_16x16.png"));
public BitmapImage ImageLarge => new BitmapImage(new Uri("pack://application:,,,/c-entron 2.0;component/Images/Icons/32x32/Thingies_32x32_32x32.png"));
public string Description => "Alle Ihre Dinge auf einen Blick";
public CentronModuleCategory MainCategory => CentronModuleCategory.MyCentron;
public string ID => "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}";
public string ReportGroupGuid => null;
public CentronConnectionType[] SupportsConnectionTypes => new[] { ronConnectionType.CentronWebServices, entronConnectionType.SqlServer }
public BaseModule CreateModuleInstance(params object[] param)
{
var viewModel = new ThingiesViewModel();
return new ThingiesView
{
DataContext = viewModel
};
}
public IList<ICentronAppModuleSettingController> GetSettings() => new <ICentronAppModuleSettingController>
{
new ThingiesSettingsController()
};
public void Dispose()
{
}
}
```
## BaseModule (View)
Next we'll create the view and as we've already seen in our `AppModuleController.CreateModuleInstance(..)` it will be called `ThingiesView`. The easiest way to create it, is just to let VisualStudio create a `User Control(WPF)` then change it's type to `BaseModule`. To do this go to the `ThingiesView.xaml.cs` and delete the inheritance of `UserControl`. Next go to `ThingiesView.xaml` and replace `UserControl` with ` controls:BaseModule`, then add this namespace: `xmlns:controls="clr-namespace:CentronSoftware.Centron.WPF.UI.Extension.Controls;assembly=Centron.WPF.UI.Extension"`
*Pro tip: add `d:DataContext="{d:DesignInstance module:TicketListModuleViewModel}"` for intelisense in xaml.*
As all our modules need a ribbon you'll need to implement one of 3 interfaces in your code-behind (`Thingies.xaml.cs`):
interface|function
--|--
IRibbonControlModule|Adds the GetRibonControl() method where your ribbon gets returned
IRibbonControlModuleWithDefaultButtons|Same as above, but also adds our default buttons (Save layout, reset layout, settings, videoportal, reports)
IRibbonControlModuleWithDefaultButtonsAndPreferredSendType|Same as above, but also adds buttons for send type for reports.
``` csharp
public partial class ThingiesView : IRibbonControlModuleWithDefaultButtons
{
public ThingiesView()
{
this.InitializeComponent();
}
public RibbonControl GetRibbonControl()
{
return this.Ribbon;
}
}
```
``` xml
<controls:BaseModule x:Class="CentronSoftware.Centron.WPF.UI.Modules.Helpdesk.TicketDetails.TicketDetailView"
xmlns:controls="clr-namespace:CentronSoftware.Centron.WPF.UI.Extension.Controls;assembly=Centron.WPF.UI.Extension"
[..]
>
[..]
<dxb:RibbonControl x:Name="Ribbon">
[..]
</dxb:RibbonControl>
[..]
</controls:BaseModule>
```
If you want to display a message box on closing or cancel closing, you can override `public override void FormClosing(out bool cancel)` and cancel the close by settings the boolean to **true**.
## ViewModel
At last we will create our viewmodel aptly named `ThingiesViewModel`. It doesnt really need anything, just implement `BindableBase` like every other viewmodel and set it as datacontext for the view in your appmodulecontroller.
## ModuleRegistration
If your module is supposed to be available via the module list on the left, then we also need to register it in `ModuleRegistration.cs`. Simply go to the constructor and add your module to the _module list like this:
``` csharp
ModuleRegistrationItem.For<ThingiesAppModuleController>(
() => Helper.NoRightCheck(),
() => ModuleFeatures.IsThingiesAvailable),
```
The first parameter allows you to check if the user has the specified rights.
The second allows you to hide your module until it is ready to be released. Simply create an entry in `ModuleFeatures.cs`.
@@ -0,0 +1,52 @@
# How to create a settings page
Naming convention for your settingspage is simple:
[YourSettingsName]Settings[Controller/View/ViewModel]
## Create your Controller
First you have to create a class that implements `ICentronAppModuleSettingController`.
`CreateSettingControlInstance()` needs to handle the creation of the view and the viewmodel and **set the DataContext for the view**. This should all be done in this method and not in the ctor of the view.
**You MUST also add an ImageLarge.**
## Create your View
Next create a view of the type `BaseSettingControl`.
You have to implement `DoInitalize()` and simply route it through to your viewmodel. It should handle all the loading of settings.
Then implement `DoSave()` and again simply route it to your viewmodel. If you want to restrict saving you can also implement `CanSave()`. `DoAfterSave()` allows you to execute something after saving of all pages is complete.
The view should not implement a WaitIndicator. One will be shown on initialization and on saving by the container.
## Create your ViewModel
The viewmodel does not need to implement anything specific aside from `BindableBase`.
It should handle initialization and loading of settings in a asnyc method called from the view. Nothing should be loaded via the ctor.
Saving should be done with another async method that is called from the view.
If there is an error during loading or saving you need to throw an exception. The container will handle it and show a error dialog. **Do NOT display a messagebox yourself**.
Of course all other c-entron<span>.NET conventions apply ;)
## Register your Controller
If your settings applies to a specific module it needs to be registered in that modules `ICentronAppModuleController.GetSettings()`.
If it does not belong to a specific module it needs to be registered in `ModuleRegistration.GetSettingsWithoutModule()`.
If your settings page is a personal settings it needs to be registered in `ModuleRegistration.GetPersonalSettings()`.
**If the specific module is not registered in the moduleslist you need to register your settings in both `ICentronAppModuleController.GetSettings()` in your module and `ModuleRegistration.GetSettingsWithoutModule()`.**
## Examples
```
Centron.WPF.UI/Modules/Administration/CentronConfigDb/CentronConfigDbSettingsController.cs
Centron.WPF.UI/Modules/Helpdesk/Settings/TypesHelpdeskTypeSettingsControllers.cs
Centron.WPF.UI/Modules/MyCentron/PersonalSettings/UI/PersonalUISettingsController.cs
```
@@ -0,0 +1,280 @@
# Localization
## General structure
We use the .NET default approach for the localization. Each DLL which needs localized strings, adds the necessary resource files.
The used language is determined by CultureInfo.CurrentUICulture.
## Resource Files Structure
The application has separate resource files for different layers and supports multiple languages:
**WPF UI Layer:**
- **German (default)**: `src/centron/Centron.WPF.UI/Resources/LocalizedStrings.resx`
- **English**: `src/centron/Centron.WPF.UI/Resources/LocalizedStrings.en.resx`
**Business Logic Layer:**
- **German (default)**: `src/backend/Centron.BL/Resources/LocalizedStrings.resx`
- **English**: `src/backend/Centron.BL/Resources/LocalizedStrings.en.resx`
### Language Support
- **Default Language**: German - stored in the base resource files (`LocalizedStrings.resx`)
- **Additional Languages**: English - stored in language-specific resource files (`LocalizedStrings.en.resx`)
- **Language Selection**: Determined by `CultureInfo.CurrentUICulture`
Each resource file contains the same keys but with text in the respective language. When creating new localized strings, you must provide translations for both German and English.
**Example of the same key in different languages:**
```
German (LocalizedStrings.resx):
LoginDialogView_Benutzername = "Benutzername"
English (LocalizedStrings.en.resx):
LoginDialogView_Benutzername = "Username"
```
## Usage Examples
### XAML Usage
In XAML files, localized strings are accessed using static property syntax with the appropriate namespace declaration.
**Namespace Declaration:**
Add this to your XAML file's root element:
```xml
xmlns:properties="clr-namespace:CentronSoftware.Centron.WPF.UI.Resources"
```
**Usage in XAML:**
```xml
<!-- For text properties -->
<Label Content="{x:Static properties:LocalizedStrings.LoginDialogView_Benutzername}" />
<!-- For placeholder text -->
<TextEdit NullText="{x:Static properties:LocalizedStrings.LoginDialogView_Benutzername}" />
<!-- For tooltip text -->
<Button ToolTip="{x:Static properties:LocalizedStrings.SomeTooltipKey}" />
```
**Real Example from LoginDialogView.xaml:**
```xml
<dxe:TextEdit Name="UsernamTextEdit"
ShowNullTextForEmptyValue="True"
NullText="{x:Static properties:LocalizedStrings.LoginDialogView_Benutzername}"
Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" />
```
### Code-Behind Usage (WPF UI)
In C# code-behind files in the WPF UI project, add the using statement and access strings directly.
**Using Statement:**
```csharp
using CentronSoftware.Centron.WPF.UI.Resources;
```
**Usage in Code:**
```csharp
// For status messages
using (CentronApplication.Instance.AppStatusBar.PushMessage(
LocalizedStrings.LoginDialogViewModel_DoLogin_Anmeldung))
{
// Your code here
}
// For exception messages
throw new Exception(LocalizedStrings.SomeErrorMessageKey);
// For dialog messages
var message = LocalizedStrings.ConfirmationMessageKey;
```
**Real Example from LoginDialogViewModel.cs:**
```csharp
using (CentronApplication.Instance.AppStatusBar.PushMessage(
LocalizedStrings.LoginDialogViewModel_DoLogin_Anmeldung))
{
var preResult = await PreDoLoginToCentron();
// ... rest of login logic
}
```
### Business Logic Usage
In business logic classes, localized strings are accessed similarly but using the business logic resource namespace.
**Using Statement:**
```csharp
using Centron.BusinessLogic.Resources;
```
**Usage in Business Logic:**
```csharp
// For exception messages
throw new ResultException(
LocalizedStrings.AppSettingsGroupBL_UpdateUpdateAvailableNotificationSettings_WählenSieMitarbeiterAusDieBeiVerfügbarenUpdatesEineBenachrichtigungAngezeigtBekommenSollen,
DefaultMessageCodes.BadRequest);
// For validation messages
var errorMessage = LocalizedStrings.ValidationErrorKey;
```
**Real Example from AppSettingsGroupBL.cs:**
```csharp
throw new ResultException(
LocalizedStrings.AppSettingsGroupBL_UpdateUpdateAvailableNotificationSettings_WählenSieMitarbeiterAusDieBeiVerfügbarenUpdatesEineBenachrichtigungAngezeigtBekommenSollen,
DefaultMessageCodes.BadRequest);
```
## Common WPF Properties Requiring Localization
When auditing XAML files for hardcoded text that needs localization, pay special attention to these classic WPF properties that commonly contain user-facing text:
### Primary Text Properties
- **`Text`** - Direct text content (TextBlock, Run, etc.)
- **`Content`** - Button content, CheckBox content, etc.
- **`Label`** - LayoutItem labels, form field labels
- **`Header`** - Headers in controls like TabItem, TreeViewItem, **GridColumn headers**
- **`Title`** - Window titles, ToolTip titles
- **`Caption`** - Ribbon page captions, group captions
### Input Control Properties
- **`NullText`** - Placeholder text in DevExpress editors
- **`Watermark`** - Placeholder text in standard controls
- **`ToolTip`** - Tooltip text for controls **and images in DataTemplates**
### Converter and Template Values
- **Converter Target Values** - Hardcoded strings in ObjectToObjectConverter mappings
- **DataTemplate Content** - Text within data templates, **including ToolTip properties on images and controls**
- **Style Setter Values** - Hardcoded text in style setters
### Grid Controls and Data Tables
- **`Header`** - GridColumn header text that displays column titles
- **Runtime Localization Strings** - Grid control runtime strings like "NoRecords" messages
- **DataTemplate ToolTips** - Tooltip text on images, buttons, and other controls within cell templates
- **Custom Grid Messages** - Any hardcoded text in grid customization areas
### Example Search Patterns
When auditing XAML files, search for these patterns:
```xml
Title="German Text"
Content="German Text"
Label="German Text"
Text="German Text"
Caption="German Text"
Header="German Text"
NullText="German Text"
Target="German Text"
ToolTip="German Text"
Value="German Text"
```
### Grid-Specific Examples
Pay special attention to grid controls:
```xml
<!-- GridColumn headers -->
<dxg:GridColumn Header="Bezeichnung" />
<dxg:GridColumn Header="Standard" />
<!-- DataTemplate tooltips -->
<Image ToolTip="Änderungen vorhanden" />
<!-- Runtime localization strings -->
<dxg:RuntimeStringIdInfo Value="Keine Verbindungen vorhanden." />
```
## Key Naming Conventions
Localization keys follow a structured naming pattern to ensure uniqueness and maintainability:
- **Format**: `[ClassName]_[MethodName]_[Description]` or `[ViewName]_[PropertyName]_[Description]`
- **Examples**:
- `LoginDialogView_Benutzername` - Username field in LoginDialogView
- `LoginDialogViewModel_DoLogin_Anmeldung` - Login process message
- `AppSettingsGroupBL_UpdateUpdateAvailableNotificationSettings_WählenSieMitarbeiterAusDieBeiVerfügbarenUpdatesEineBenachrichtigungAngezeigtBekommenSollen` - Business logic validation message
## Tools
#### ResXManager
Great tool for managing the language resource files. It is a visual studio extension. It provides a dialog too manage the resource and translate the resources.
It also adds a right click menu item "Move to Resource" for quick resource entry creation.
## Working
If we encounter strings which are localized through the old workflow (CentronLocalization.Instance.GetLocalizedString()) we replace it with the new workflow.
### Translate strings in code
1. Click into the string. (Complete string selection is not nessassry. The VS-Extension extends the selection automaticly. Be careful if your string contains ". In this case check if the complete text got selected. If not, select manualy.)
2. Right Click.
3. Select "Move to Resource".
4. The popup "Move to Resource" appears.
- For better read ability select at "Code" the format which adds the class and method name too the generated code.
- If the string contains variables {0}, {1} add a "Comment" for them. So we have a info in the resource file what values can be expected in the variables.
- After a klick on "OK" the extension creates a new entry in the resource file and replaces the string with the generated code property.
#### Namespaces
.xaml: `xmlns:properties="clr-namespace:CentronSoftware.Centron.WPF.UI.Resources"`
.cs: `using CentronSoftware.Centron.WPF.UI.Resources;`
#### Useful searches
These can be used by using the inbuilt search function: Open 'Find and Replace', disable 'Match case' and 'Match whole word', enable 'Use regular expressions' then select 'Currrent document' and 'Entire document'. Now you can just go through the file using 'Find Next' and you should catch most strings.
.cs:
`"[^"]*?"` - finds all strings
`(?=(^((?!///).)*$)).*((".+?")|('.+?')).*` - finds all strings excluding comments
`(LocalizationManager|CentronLocalization)\.Instance\.GetLocalizedString\("[0-9]*", "[^"]*?"\);` - finds texts already translated by the old system
.xaml:
`(Text|Label|Caption|ToolTip|Content|Header)="[^{]*?"` - finds most common string literals
`{lex:Loc[^"]*?"` - finds texts already translated by the old system
#### Keybindings
1. Open `Tools`>`Options`>`Keyboard`
2. Search for `EditorContextMenus.CodeWindow.Movetoresource`
3. Bind to the keys you like (don't foregt to press Assign)
4. Profit
### Translate entries in the resource file
1. To translate a resource entry, navigate to the Resource directory in your assembly.
2. Locate the file "LocalizedStrings.resx".
3. Right-Click it and select "Open in ResX Manager".
If the Resource Manager shows no files, click on "Refresh view".
- The manager highlights the not translated entries with a red background color.
- Additionaly this Tool contains a own tab for the translation. It shows the entries which are not translated. There is a auto translate function too.
## Setup localization for a assembly
1. Create a "Resources" directory, if not existing.
2. Add "New Item"
3. Select "Resources File"
4. Use as Name: LocalizedStrings.resx
5. Press "Add"-Button
6. If visual studio does open the resource file automaticly, close it.
7. Select the new file in the Solution Explorer.
8. Check The properties.
- Build Action: Embedded resource
- Custom Tool: ResXFileCodeGenerator. Enter PublicResXFileCodeGenerator if the assembly contains XAML UI.
9. Open "ResX Manager" through Tools (Visual Studio menu)
10. Click on "Refresh View", the new resource file should be visibly now.
11. Close the manager.
12. Translate a string in the source code.
13. Open the ResX Manager again.
14. If not selected, select the new resource file.
15. Because we have no resource files for the other languages yet, double click into the edit box and add a translation for the language. The tool will create a new resource file for the language.
16. Done. We can close the manager.
Its possible that the Resource Manager does not show the new created langugage files from the start. In this case press Refresh. This should fix the issue.
## Get localized strings through Web-Service call
To get the message in a specific langugage, it is necessary too add a new Attribut too the header "Accept-Language" (Value example: de-DE).
Example:
httpRequest.Headers.Add("Accept-Language", CultureInfo.CurrentUICulture.Name);