Skip to content

Commit ebcc2b3

Browse files
committed
Merge branch 'feature/ReflexiveAttribute' into develop
Currently, the `AttributeDescriptor` has two attributes—`DefaultValue` and `ImplicitValue`—which are intended to collect optional values for the `AttributeDescriptor`. The problem is that these are always displayed as a text box, and stored as an attribute. That means that if the `AttributeDescriptor` is a `BooleanAttributeDescriptor`, the editor needs to know to enter `1` or `0`; if it's a `TopicListAttributeDescriptor`, they need to know which property to enter (e.g., `TopicId`, `Key`, `Title`, &c.); if it's a `TopicReference`, they need to know the `Topic.Id` of the target type, and know to enter it. This is confusing, and error prone. It also means that `DefaultValue` and `ImplicitValue` are solicited for attribute types that they don't make much sense for—such as relationships and nested topics. To mitigate this, we've introduced a new `ReflexiveAttribute` (#44), which can be placed on an `AttributeDescriptor` to reflect the configuration of the `AttributeDescriptor` back to itself. So on a `TopicReferenceAttribute`, the `ReflexiveAttribute` will render a `TopicReferenceViewComponent` with the same configuration as the current `TopicReferenceAttribute`, and store the value as a topic reference. This is accomplished by mapping the `CurrentTopic` as an `AttributeDescriptorViewModel` in the `ReflexiveViewComponent` (aebcc6f), so that the view can invoke the `CurrentTopic` as an attribute view component using the `AttributeDescriptorViewModel` it expects, with the current values configured for the `AttributeDescriptorViewModel` (2f44fc1). To accomplish this, a dependency on the `ITypeLookupService` and `ITopicMappingService` (aebcc6f) are introduced, which have been added to the `StandardEditorComposer` (3efa14f). Unfortunately, adding these to the constructor, as we'd usually do with dependencies—and which is a best practice for dependency injection!—isn't possibly without breaking the OnTopic Editor 5.0.0 contract, and thus this change will need to wait until OnTopic Editor 6.0.0. Positively, these are well-known objects which will always be available—since one is local, and the other is from the core `OnTopic` library—and these aren't dependencies we'd expect implementors to need to swap out. Therefore, for backward compatibility, they're being hard-coded into the `StandardEditorComposer`. As this operates as part of the composition root, and implementors can always wire up attribute view components using their own custom logic without relying on the `StandardEditorComposer`, this isn't a huge issue, but it's still a bit ugly, and something we'll want to fix with OnTopic Editor 6.0.0.
2 parents bd9de01 + 7bcd4a3 commit ebcc2b3

10 files changed

Lines changed: 239 additions & 10 deletions

File tree

OnTopic.Editor.AspNetCore.Attributes/OnTopic.Editor.AspNetCore.Attributes.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<PrivateAssets>all</PrivateAssets>
2626
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2727
</PackageReference>
28-
<PackageReference Include="OnTopic" Version="5.1.0-alpha.482" />
28+
<PackageReference Include="OnTopic" Version="5.1.0-alpha.517" />
2929
</ItemGroup>
3030

3131
<ItemGroup>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using OnTopic.Metadata;
7+
8+
namespace OnTopic.Editor.AspNetCore.Attributes.ReflexiveAttribute {
9+
10+
/*============================================================================================================================
11+
| CLASS: REFLEXIVE ATTRIBUTE (DESCRIPTOR)
12+
\---------------------------------------------------------------------------------------------------------------------------*/
13+
/// <summary>
14+
/// Represents metadata for describing a reflexive attribute type, including information on how it will be presented and
15+
/// validated in the editor.
16+
/// </summary>
17+
/// <remarks>
18+
/// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the
19+
/// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself.
20+
/// </remarks>
21+
public class ReflexiveAttributeDescriptor : AttributeDescriptor {
22+
23+
/*==========================================================================================================================
24+
| CONSTRUCTOR
25+
\-------------------------------------------------------------------------------------------------------------------------*/
26+
/// <inheritdoc />
27+
public ReflexiveAttributeDescriptor(
28+
string key,
29+
string contentType,
30+
Topic parent,
31+
int id = -1
32+
) : base(
33+
key,
34+
contentType,
35+
parent,
36+
id
37+
) {
38+
39+
}
40+
41+
/*==========================================================================================================================
42+
| PROPERTY: MODEL TYPE
43+
\-------------------------------------------------------------------------------------------------------------------------*/
44+
/// <inheritdoc />
45+
public override ModelType ModelType => ModelType.Reflexive;
46+
47+
} //Class
48+
} //Namespace
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using OnTopic.Editor.AspNetCore.Models.Metadata;
7+
8+
namespace OnTopic.Editor.AspNetCore.Attributes.ReflexiveAttribute {
9+
10+
/*============================================================================================================================
11+
| CLASS: REFLEXIVE ATTRIBUTE DESCRIPTOR (VIEW MODEL)
12+
\---------------------------------------------------------------------------------------------------------------------------*/
13+
/// <summary>
14+
/// Provides access to attributes associated with the <see cref="ReflexiveViewComponent"/>.
15+
/// </summary>
16+
public record ReflexiveAttributeDescriptorViewModel: AttributeDescriptorViewModel {
17+
18+
19+
} //Class
20+
} //Namespace
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Mvc;
8+
using OnTopic.Editor.AspNetCore.Models;
9+
using OnTopic.Editor.AspNetCore.Models.Metadata;
10+
using OnTopic.Internal.Diagnostics;
11+
using OnTopic.Mapping;
12+
using OnTopic.Repositories;
13+
14+
namespace OnTopic.Editor.AspNetCore.Attributes.ReflexiveAttribute {
15+
16+
/*============================================================================================================================
17+
| CLASS: REFLEXIVE (VIEW COMPONENT)
18+
\---------------------------------------------------------------------------------------------------------------------------*/
19+
/// <summary>
20+
/// Delivers a view model for a reflexive attribute type.
21+
/// </summary>
22+
public class ReflexiveViewComponent : ViewComponent {
23+
24+
/*==========================================================================================================================
25+
| PRIVATE VARIABLES
26+
\-------------------------------------------------------------------------------------------------------------------------*/
27+
private readonly ITopicRepository _topicRepository;
28+
private readonly ITopicMappingService _topicMappingService;
29+
30+
/*==========================================================================================================================
31+
| CONSTRUCTOR
32+
\-------------------------------------------------------------------------------------------------------------------------*/
33+
/// <summary>
34+
/// Initializes a new instance of a <see cref="ReflexiveViewComponent"/> with necessary dependencies.
35+
/// </summary>
36+
public ReflexiveViewComponent(ITopicRepository topicRepository, ITopicMappingService topicMappingService) : base() {
37+
_topicRepository = topicRepository;
38+
_topicMappingService = topicMappingService;
39+
}
40+
41+
/*==========================================================================================================================
42+
| METHOD: INVOKE
43+
\-------------------------------------------------------------------------------------------------------------------------*/
44+
/// <summary>
45+
/// Assembles the view model for the <see cref="ReflexiveViewComponent"/>.
46+
/// </summary>
47+
public async Task<IViewComponentResult> InvokeAsync(
48+
EditingTopicViewModel currentTopic,
49+
ReflexiveAttributeDescriptorViewModel attribute,
50+
string htmlFieldPrefix
51+
) {
52+
53+
/*------------------------------------------------------------------------------------------------------------------------
54+
| Validate parameters
55+
\-----------------------------------------------------------------------------------------------------------------------*/
56+
Contract.Requires(currentTopic, nameof(currentTopic));
57+
Contract.Requires(attribute, nameof(attribute));
58+
59+
/*------------------------------------------------------------------------------------------------------------------------
60+
| Set HTML prefix
61+
\-----------------------------------------------------------------------------------------------------------------------*/
62+
ViewData.TemplateInfo.HtmlFieldPrefix = htmlFieldPrefix;
63+
64+
/*------------------------------------------------------------------------------------------------------------------------
65+
| Establish snapshot of previously saved attribute descriptor
66+
\-----------------------------------------------------------------------------------------------------------------------*/
67+
var topic = _topicRepository.Load(currentTopic.UniqueKey);
68+
var reflexiveViewModel = (AttributeDescriptorViewModel?)await _topicMappingService.MapAsync(topic).ConfigureAwait(false);
69+
70+
/*------------------------------------------------------------------------------------------------------------------------
71+
| Establish hybrid view model
72+
>-------------------------------------------------------------------------------------------------------------------------
73+
| The ParentAttributeDescriptor will be of the target type expected for the view component that will be executed. But it
74+
| should use the core AttributeDescriptor attributes of the current attribute, so it shows up in the same location with
75+
| the same title and description as defined for the ReflexiveAttributeDescriptorViewModel.
76+
\-----------------------------------------------------------------------------------------------------------------------*/
77+
reflexiveViewModel = reflexiveViewModel with {
78+
Key = attribute.Key,
79+
Description = attribute.Description,
80+
DisplayGroup = attribute.DisplayGroup,
81+
DefaultValue = attribute.DefaultValue,
82+
IsRequired = attribute.IsRequired,
83+
SortOrder = attribute.SortOrder,
84+
Title = attribute.Title
85+
};
86+
87+
/*------------------------------------------------------------------------------------------------------------------------
88+
| Establish view model
89+
\-----------------------------------------------------------------------------------------------------------------------*/
90+
var viewModel = new AttributeViewModel<AttributeDescriptorViewModel>(currentTopic, reflexiveViewModel);
91+
92+
/*------------------------------------------------------------------------------------------------------------------------
93+
| Return view with view model
94+
\-----------------------------------------------------------------------------------------------------------------------*/
95+
return View(viewModel);
96+
97+
}
98+
99+
} // Class
100+
} // Namespace

OnTopic.Editor.AspNetCore.Attributes/StandardEditorComposer.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using OnTopic.Editor.AspNetCore.Attributes.MetadataListAttribute;
1919
using OnTopic.Editor.AspNetCore.Attributes.NestedTopicListAttribute;
2020
using OnTopic.Editor.AspNetCore.Attributes.NumberAttribute;
21+
using OnTopic.Editor.AspNetCore.Attributes.ReflexiveAttribute;
2122
using OnTopic.Editor.AspNetCore.Attributes.RelationshipAttribute;
2223
using OnTopic.Editor.AspNetCore.Attributes.TextAreaAttribute;
2324
using OnTopic.Editor.AspNetCore.Attributes.TextAttribute;
@@ -26,7 +27,10 @@
2627
using OnTopic.Editor.AspNetCore.Attributes.TopicReferenceAttribute;
2728
using OnTopic.Editor.AspNetCore.Components;
2829
using OnTopic.Editor.AspNetCore.Controllers;
30+
using OnTopic.Editor.AspNetCore.Infrastructure;
2931
using OnTopic.Internal.Diagnostics;
32+
using OnTopic.Lookup;
33+
using OnTopic.Mapping;
3034
using OnTopic.Repositories;
3135

3236
namespace OnTopic.Editor.AspNetCore.Attributes {
@@ -49,6 +53,8 @@ public class StandardEditorComposer {
4953
\-------------------------------------------------------------------------------------------------------------------------*/
5054
private readonly ITopicRepository _topicRepository;
5155
private readonly IWebHostEnvironment _webHostEnvironment;
56+
private readonly ITypeLookupService _typeLookupService;
57+
private readonly ITopicMappingService _topicMappingService;
5258

5359
/*==========================================================================================================================
5460
| CONSTRUCTOR
@@ -76,6 +82,8 @@ public StandardEditorComposer(ITopicRepository topicRepository, IWebHostEnvironm
7682
\-----------------------------------------------------------------------------------------------------------------------*/
7783
_topicRepository = topicRepository;
7884
_webHostEnvironment = webHostEnvironment;
85+
_typeLookupService = new EditorViewModelLookupService();
86+
_topicMappingService = new TopicMappingService(_topicRepository, _typeLookupService);
7987

8088
}
8189

@@ -121,6 +129,7 @@ ITopicRepository topicRepository
121129
nameof(MetadataListViewComponent) => new MetadataListViewComponent(),
122130
nameof(NestedTopicListViewComponent) => new NestedTopicListViewComponent(_topicRepository),
123131
nameof(NumberViewComponent) => new NumberViewComponent(),
132+
nameof(ReflexiveViewComponent) => new ReflexiveViewComponent(_topicRepository, _topicMappingService),
124133
nameof(RelationshipViewComponent) => new RelationshipViewComponent(),
125134
nameof(TextViewComponent) => new TextViewComponent(),
126135
nameof(TextAreaViewComponent) => new TextAreaViewComponent(),
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@using OnTopic.Metadata;
2+
@model AttributeViewModel<AttributeDescriptorViewModel>
3+
4+
@{
5+
6+
var excludedModelTypes = new ModelType[] { ModelType.Reflexive, ModelType.Relationship, ModelType.NestedTopic };
7+
var modelType = Model.AttributeDescriptor.ModelType;
8+
var isSupported = !excludedModelTypes.Contains(modelType);
9+
10+
if (!isSupported) {
11+
Layout = "~/Areas/Editor/Views/Editor/Components/_Layout.cshtml";
12+
}
13+
14+
}
15+
16+
@if (isSupported) {
17+
<text>
18+
@await Component.InvokeAsync(
19+
Model.CurrentTopic.ContentType.Replace("AttributeDescriptor", ""),
20+
new { CurrentTopic = Model.CurrentTopic, Attribute = Model.AttributeDescriptor, HtmlFieldPrefix = Html.ViewData.TemplateInfo.HtmlFieldPrefix }
21+
)
22+
</text>
23+
}
24+
else {
25+
<div class="alert alert-warning" role="alert">
26+
<strong>Note:</strong> This attribute cannot be displayed on topics implementing the @Model.CurrentTopic.ContentType content type.
27+
</div>
28+
}

OnTopic.Editor.AspNetCore.Host/OnTopic.Editor.AspNetCore.Host.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
<PrivateAssets>all</PrivateAssets>
1212
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1313
</PackageReference>
14-
<PackageReference Include="OnTopic.Data.Caching" Version="5.1.0-alpha.482" />
15-
<PackageReference Include="OnTopic.Data.Sql" Version="5.1.0-alpha.482" />
14+
<PackageReference Include="OnTopic.Data.Caching" Version="5.1.0-alpha.517" />
15+
<PackageReference Include="OnTopic.Data.Sql" Version="5.1.0-alpha.517" />
1616
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.4" />
1717
</ItemGroup>
1818

OnTopic.Editor.AspNetCore/Controllers/EditorController.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,27 @@ private ContentTypeDescriptor GetContentType(string contentType) => TopicReposit
107107
.FirstOrDefault()??
108108
(ContentTypeDescriptor)TopicFactory.Create(contentType, "ContentTypeDescriptor");
109109

110+
/*==========================================================================================================================
111+
| GET MODEL TYPE
112+
\-------------------------------------------------------------------------------------------------------------------------*/
113+
/// <summary>
114+
/// Given the <see cref="AttributeDescriptor.ModelType"/>, returns the current <see cref="ModelType"/>.
115+
/// </summary>
116+
/// <remarks>
117+
/// Typically, the <see cref="AttributeDescriptor.ModelType"/> is the current <see cref="ModelType"/>. The one exception
118+
/// is if the <see cref="AttributeDescriptor.ModelType"/> is set to <see cref="ModelType.Reflexive"/>, in which case the
119+
/// <see cref="ModelType"/> should instead be pulled from the <see cref="CurrentTopic"/>—which should be a <see cref="
120+
/// AttributeDescriptor"/>.
121+
/// </remarks>
122+
/// <param name="modelType"></param>
123+
/// <returns></returns>
124+
private ModelType GetModelType(ModelType modelType) {
125+
if (modelType == ModelType.Reflexive && CurrentTopic is AttributeDescriptor) {
126+
return ((AttributeDescriptor)CurrentTopic).ModelType;
127+
}
128+
return modelType;
129+
}
130+
110131
/*==========================================================================================================================
111132
| GET EDITOR VIEW MODEL
112133
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -170,19 +191,21 @@ bool isModal
170191
//The attribute collections follow special conventions that can't be automatically mapped from the topic
171192
foreach (var attribute in contentTypeDescriptor.AttributeDescriptors) {
172193

194+
var modelType = GetModelType(attribute.ModelType);
195+
173196
//For new topics that aren't derived from another topic, assign attribute default, if available
174197
if (isNew) {
175198
topicViewModel.Attributes.Add(attribute.Key, CurrentTopic.BaseTopic is null? null : attribute.DefaultValue);
176199
}
177200

178201
//Serialize relationships, if it's a relationship type
179-
else if (attribute.ModelType is ModelType.Relationship) {
202+
else if (modelType is ModelType.Relationship) {
180203
var relatedTopicIds = CurrentTopic.Relationships.GetValues(attribute.Key).Select<Topic, int>(m => m.Id).ToArray();
181204
topicViewModel.Attributes.Add(attribute.Key, String.Join(",", relatedTopicIds));
182205
}
183206

184207
//Serialize references, if it's a topic reference
185-
else if (attribute.ModelType is ModelType.Reference) {
208+
else if (modelType is ModelType.Reference) {
186209
topicViewModel.Attributes.Add(attribute.Key, CurrentTopic.References.GetValue(attribute.Key, null, false, false)?.Id.ToString(CultureInfo.InvariantCulture));
187210
}
188211

@@ -402,14 +425,16 @@ public async Task<IActionResult> Edit(
402425
continue;
403426
}
404427

428+
var modelType = GetModelType(attribute.ModelType);
429+
405430
//Get reference to current attribute
406431
var attributeValue = model.Attributes[attribute.Key];
407432

408433
//Save value
409-
if (attribute.ModelType is ModelType.Relationship) {
434+
if (modelType is ModelType.Relationship) {
410435
SetRelationships(topic, attribute, attributeValue);
411436
}
412-
else if (attribute.ModelType is ModelType.Reference) {
437+
else if (modelType is ModelType.Reference) {
413438
SetReference(topic, attribute, attributeValue);
414439
}
415440
else if (attribute.Key is "Key") {

OnTopic.Editor.AspNetCore/Models/EditingTopicViewModel.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System;
77
using System.Collections.Generic;
88
using System.Collections.ObjectModel;
9-
using System.Diagnostics.CodeAnalysis;
109
using OnTopic.Editor.AspNetCore.Models.Metadata;
1110
using OnTopic.Mapping.Annotations;
1211
using OnTopic.Metadata;

OnTopic.Editor.AspNetCore/OnTopic.Editor.AspNetCore.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
<PrivateAssets>all</PrivateAssets>
2525
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2626
</PackageReference>
27-
<PackageReference Include="OnTopic" Version="5.1.0-alpha.482" />
27+
<PackageReference Include="OnTopic" Version="5.1.0-alpha.517" />
2828
<PackageReference Include="OnTopic.Data.Transfer" Version="2.0.0" />
29-
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="5.1.0-alpha.482" />
29+
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="5.1.0-alpha.517" />
3030
</ItemGroup>
3131

3232
</Project>

0 commit comments

Comments
 (0)