Skip to content

Commit f35d88d

Browse files
authored
Merge pull request #83 from Namoshek/feature/model-property-aliases
Add new base model for DisplayName support
2 parents a1c79ca + 6976edb commit f35d88d

File tree

3 files changed

+125
-4
lines changed

3 files changed

+125
-4
lines changed

DocxTemplater.Test/DocxTemplateTest.cs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
using DocumentFormat.OpenXml;
1+
using System.Collections;
2+
using System.ComponentModel;
3+
using System.Dynamic;
4+
using System.Globalization;
5+
using DocumentFormat.OpenXml;
26
using DocumentFormat.OpenXml.Packaging;
37
using DocumentFormat.OpenXml.Wordprocessing;
48
using DocxTemplater.Images;
59
using DocxTemplater.Model;
6-
using System.Collections;
7-
using System.Dynamic;
8-
using System.Globalization;
910
using Bold = DocumentFormat.OpenXml.Wordprocessing.Bold;
1011
using Break = DocumentFormat.OpenXml.Drawing.Break;
1112
using Paragraph = DocumentFormat.OpenXml.Wordprocessing.Paragraph;
@@ -1326,6 +1327,31 @@ public void TestWithNestedITemplateModel_AdditionalDataIsNull()
13261327
"</w:r></w:p>"));
13271328
}
13281329

1330+
[Test]
1331+
public void TestWithBaseTemplateModelWithDisplayNames()
1332+
{
1333+
using var memStream = new MemoryStream();
1334+
using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document);
1335+
1336+
MainDocumentPart mainPart = wpDocument.AddMainDocumentPart();
1337+
mainPart.Document = new Document(new Body(new Paragraph(
1338+
new Run(new Text("{{ds.Vorname}} {{ds.LastName}} aus {{ds.Anschrift}}"))
1339+
)));
1340+
wpDocument.Save();
1341+
memStream.Position = 0;
1342+
var docTemplate = new DocxTemplate(memStream);
1343+
docTemplate.RegisterFormatter(new Markdown.MarkdownFormatter());
1344+
docTemplate.BindModel("ds", new DummyModelWithDisplayNames());
1345+
var result = docTemplate.Process();
1346+
docTemplate.Validate();
1347+
Assert.That(result, Is.Not.Null);
1348+
result.Position = 0;
1349+
1350+
var document = WordprocessingDocument.Open(result, false);
1351+
var body = document.MainDocumentPart.Document.Body;
1352+
Assert.That(body.InnerText, Is.EqualTo("John Doe aus Musterstraße 42, A-4242 Musterhausen"));
1353+
}
1354+
13291355
private static DriveStudentOverviewReportingModel CrateBillTemplate2Model()
13301356
{
13311357
DriveStudentOverviewReportingModel model = new()
@@ -1492,5 +1518,17 @@ public bool TryGetPropertyValue(string propertyName, out ValueWithMetadata value
14921518
return false;
14931519
}
14941520
}
1521+
1522+
private class DummyModelWithDisplayNames : TemplateModelWithDisplayNames
1523+
{
1524+
[DisplayName("Vorname")]
1525+
public string FirstName { get; set; } = "John";
1526+
1527+
public string LastName { get; set; } = "Doe";
1528+
1529+
[DisplayName("Anschrift")]
1530+
[ModelProperty(DefaultFormatter = "md")]
1531+
public string Address { get; set; } = "Musterstraße 42, **A-4242 Musterhausen**";
1532+
}
14951533
}
14961534
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Linq;
4+
using System.Reflection;
5+
6+
namespace DocxTemplater.Model
7+
{
8+
public abstract class TemplateModelWithDisplayNames : ITemplateModel
9+
{
10+
public bool TryGetPropertyValue(string propertyName, out ValueWithMetadata value)
11+
{
12+
var properties = GetType().GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance);
13+
14+
// Try to find property with the specified display name.
15+
foreach (var property in properties)
16+
{
17+
var displayNameAttribute = property.GetCustomAttribute<DisplayNameAttribute>();
18+
if (displayNameAttribute is null || !string.Equals(displayNameAttribute.DisplayName, propertyName, StringComparison.OrdinalIgnoreCase))
19+
{
20+
continue;
21+
}
22+
23+
return GetValue(property, out value);
24+
}
25+
26+
// Try to find property with the specified name (case-insensitive) as fallback.
27+
var propertyByName = properties.FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase));
28+
if (propertyByName != null)
29+
{
30+
return GetValue(propertyByName, out value);
31+
}
32+
33+
value = new ValueWithMetadata(null);
34+
return false;
35+
}
36+
37+
private bool GetValue(PropertyInfo property, out ValueWithMetadata value)
38+
{
39+
var modelPropertyAttribute = property.GetCustomAttribute<ModelPropertyAttribute>();
40+
41+
value = new ValueWithMetadata(property.GetValue(this), new ValueMetadata(modelPropertyAttribute?.DefaultFormatter));
42+
return true;
43+
}
44+
}
45+
}

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,44 @@ var docTemplate = new DocxTemplate(memStream, new ProcessSettings()
327327
var result = docTemplate.Process();
328328
```
329329

330+
## Advanced Model Binding: `ITemplateModel` and `TemplateModelWithDisplayNames`
331+
332+
### `ITemplateModel` Interface
333+
334+
For advanced scenarios where a standard object or dictionary is not suitable for your data model, you can implement the `ITemplateModel` interface.
335+
This allows you to control how properties are resolved for template binding.
336+
337+
```csharp
338+
public interface ITemplateModel
339+
{
340+
bool TryGetPropertyValue(string propertyName, out ValueWithMetadata value);
341+
}
342+
```
343+
344+
Implement this interface to provide custom property lookup logic.
345+
This is useful for dynamic models, computed properties, or when you want to support custom property resolution strategies.
346+
347+
### `TemplateModelWithDisplayNames` Base Class
348+
349+
`TemplateModelWithDisplayNames` is an abstract base class that extends `ITemplateModel` and allows you to bind template placeholders to properties using either their property name or a `[DisplayName]` attribute.
350+
This is especially useful when you want to use user-friendly or localized names in your templates.
351+
352+
```csharp
353+
using DocxTemplater.Model;
354+
using System.ComponentModel;
355+
356+
public class PersonModel : TemplateModelWithDisplayNames
357+
{
358+
[DisplayName("Vorname")]
359+
public string FirstName { get; set; }
360+
361+
[DisplayName("Nachname")]
362+
public string LastName { get; set; }
363+
}
364+
```
365+
366+
In your template, you can then use either `{{person.Vorname}}` or `{{person.FirstName}}` to access the property.
367+
330368
## Support This Project
331369

332370
If you find DocxTemplater useful, please consider supporting its development:

0 commit comments

Comments
 (0)