Skip to content

Commit 0eb8f2f

Browse files
committed
Add single source of truth tutorial
1 parent 69253aa commit 0eb8f2f

File tree

4 files changed

+158
-14
lines changed

4 files changed

+158
-14
lines changed

contents/trees/dotnet9-blazor-wasm.json

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,27 @@
8080
"GitHubLink": "https://github.com/Blazor-School/blazor-school-free-course-dotnet9-wasm/tree/master/ComponentCommunication",
8181
"Children":
8282
[
83-
{
84-
"ContentPath": "cascading-parameter/v1",
85-
"GitHubLink": "https://github.com/Blazor-School/blazor-school-free-course-dotnet9-wasm/tree/master/CascadingParameterInDetail",
86-
"Children":
87-
[
88-
]
89-
},
90-
{
91-
"ContentPath": "parameter/v1",
92-
"GitHubLink": "https://github.com/Blazor-School/blazor-school-free-course-dotnet9-wasm/tree/master/ParameterInDetail",
93-
"Children":
94-
[
95-
]
96-
}
83+
{
84+
"ContentPath": "cascading-parameter/v1",
85+
"GitHubLink": "https://github.com/Blazor-School/blazor-school-free-course-dotnet9-wasm/tree/master/CascadingParameterInDetail",
86+
"Children":
87+
[
88+
]
89+
},
90+
{
91+
"ContentPath": "parameter/v1",
92+
"GitHubLink": "https://github.com/Blazor-School/blazor-school-free-course-dotnet9-wasm/tree/master/ParameterInDetail",
93+
"Children":
94+
[
95+
]
96+
},
97+
{
98+
"ContentPath": "component-single-source-of-truth/v1",
99+
"GitHubLink": "https://github.com/Blazor-School/blazor-school-free-course-dotnet9-wasm/tree/master/SingleSourceOfTruth",
100+
"Children":
101+
[
102+
]
103+
}
97104
]
98105
}
99106
]
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<ul>
2+
<li>Single Source of Truth</li>
3+
</ul>
4+
<p>In modern websites, some pieces of information may appear in multiple places, such as the profile avatar, the website language, theme, etc. One way to ensure data consistency and accuracy is to implement the Single Source of Truth (SSOT) pattern, which centralises all data instead of distributing it across multiple components. In this tutorial, we will go through:</p>
5+
<ul>
6+
<li>The need for a single source of truth.</li>
7+
<li>Implementation approaches.</li>
8+
<li><code>CascadingValueSource</code> approach.</li>
9+
<li>Scoped Service approach.</li>
10+
</ul>
11+
<div class="ratio ratio-16x9"><iframe width="560" height="315" src="https://www.youtube.com/embed/83Xc4SA8q54?si=PDY8K59mZWQqzywy" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
12+
<hr class="my-4" />
13+
<h1>The Need For a Single Source of Truth</h1>
14+
<p>In Blazor, maintaining consistent data across components is essential, particularly in features like profile management. Consider the scenario profile management as in the image:</p>
15+
<p><img src="tutorials/component-single-source-of-truth/v1/why-ssot.webp" alt="why-ssot.webp" width="777" height="586" /></p>
16+
<p>When the user inputs data and the change is accepted, it should update the display name on the menu bar and the profile page. Without SSOT, developers might risk:</p>
17+
<ul>
18+
<li>Data inconsistency and inaccuracy.</li>
19+
<li>Difficulty in updating data in multiple places.</li>
20+
<li>An unfriendly SPA where users have to reload the page to see new data.</li>
21+
</ul>
22+
<p>With data centralised with SSOT, developers can easily find the data needed to display. Furthermore, when changes are applied to the data source, all components will be automatically updated. This ensures data consistency and accuracy throughout the app and reduces the effort to maintain such data.</p>
23+
<hr class="my-4" />
24+
<h1>Implementation Approaches</h1>
25+
<p>There are 2 ways to implement the SSOT pattern in Blazor:</p>
26+
<ul>
27+
<li>Using <code>CascadingValueSource</code>: This method leverages a cascading parameter with a source that automatically notifies and updates all dependent components when the central data changes. It&rsquo;s ideal for real-time updates, such as reflecting a new display name across the app, with minimal manual intervention.</li>
28+
<li>Using a Scoped Service: This approach involves a service registered as scoped, requiring manual updates to components via event handling or state management. It offers more control but demands additional code, making it suitable for legacy projects or complex state logic.</li>
29+
</ul>
30+
<h3>Best practice</h3>
31+
<p>For new projects, <code>CascadingValueSource</code> is typically the better option due to its simplicity and automatic updates. Use a scoped service when integrating with older codebases.</p>
32+
<hr class="my-4" />
33+
<h1><code>CascadingValueSource</code> Approach</h1>
34+
<ol>
35+
<li><strong>Define the data-holding class</strong>: Create a class with a <code>CascadingValueSource&lt;T&gt;</code> property, where <code>T</code> is the class itself:</li>
36+
</ol>
37+
<pre language="csharp">public class CascadingParameterSharedData
38+
{
39+
public int Value { get; set; }
40+
41+
public CascadingValueSource&lt;CascadingParameterSharedData&gt; Source { get; set; }
42+
}</pre>
43+
<ol start="2">
44+
<li><strong>Register the class in </strong><code>Program.cs</code>: While registering the class, set the source value.</li>
45+
</ol>
46+
<pre language="csharp">builder.Services.AddCascadingValue(sp =&gt;
47+
{
48+
var value = new CascadingParameterSharedData();
49+
value.Source = new(value, false); // false indicates no fixed value
50+
51+
return value.Source;
52+
});</pre>
53+
<ol start="3">
54+
<li><strong>Use the Cascading Parameter in Components</strong>:</li>
55+
</ol>
56+
<pre language="razor">&lt;div&gt;Value: @SharedData.Value&lt;/div&gt;
57+
&lt;button type="button" @onclick="ChangeValue"&gt;Change Value&lt;/button&gt;
58+
59+
@code {
60+
[CascadingParameter]
61+
public CascadingParameterSharedData SharedData { get; set; }
62+
63+
public async Task ChangeValue()
64+
{
65+
var random = new Random();
66+
SharedData.Value = random.Next(1, 100);
67+
await SharedData.Source.NotifyChangedAsync();
68+
}
69+
}</pre>
70+
<blockquote>After modifying the shared data (e.g., <code>SharedData.Value</code>), always invoke <code>NotifyChangedAsync</code> to propagate the change. Failing to do so will leave other components using the old value, breaking the SSOT principle and causing rendering discrepancies.</blockquote>
71+
<hr class="my-4" />
72+
<h1>Scoped Service Approach</h1>
73+
<ol>
74+
<li><strong>Define the data-holding class</strong>: Create a class with data and a notification mechanism:</li>
75+
</ol>
76+
<pre language="csharp">public class BlazorSchoolTransferService
77+
{
78+
public string Message { get; set; } = "";
79+
public event EventHandler MessageChanged = (sender, args) =&gt; { };
80+
81+
public void NotifyChanged()
82+
{
83+
MessageChanged.Invoke(this, EventArgs.Empty);
84+
}
85+
}</pre>
86+
<ol start="2">
87+
<li><strong>Register the class in</strong> <code>Program.cs</code>:</li>
88+
</ol>
89+
<pre language="csharp">builder.Services.AddScoped&lt;BlazorSchoolTransferService&gt;();</pre>
90+
<ol start="3">
91+
<li><strong>Inject and use the service in components</strong>:</li>
92+
</ol>
93+
<pre language="razor">@inject BlazorSchoolTransferService TransferService
94+
@implements IDisposable
95+
96+
&lt;div class="bg-primary p-5"&gt;
97+
&lt;h3&gt;Component1&lt;/h3&gt;
98+
&lt;div&gt;Value: @TransferService.Message&lt;/div&gt;
99+
&lt;button type="button" @onclick="ChangeValue"&gt;Change Value&lt;/button&gt;
100+
&lt;Component2 /&gt;
101+
&lt;/div&gt;
102+
103+
@code {
104+
protected override void OnInitialized()
105+
{
106+
TransferService.MessageChanged += OnTransferServiceChanged;
107+
}
108+
109+
public void OnTransferServiceChanged(object? sender, EventArgs e)
110+
{
111+
StateHasChanged();
112+
}
113+
114+
public void ChangeValue()
115+
{
116+
var random = new Random();
117+
TransferService.Message = "Message updated from Component1";
118+
TransferService.NotifyChanged();
119+
}
120+
121+
public void Dispose()
122+
{
123+
TransferService.MessageChanged -= OnTransferServiceChanged;
124+
}
125+
}</pre>
126+
<blockquote>
127+
<ul>
128+
<li><strong>Event Subscription</strong>: Every component accessing data must subscribe to change event (e.g., in <code>OnInitialized</code>) and call <code>StateHasChanged</code> to update the UI when notified. The number of events depends on the requirement. It could be one event for multiple properties or one event per property.</li>
129+
<li><strong>Notification</strong>: After modifying data, always call notify method to propagate the change to all subscribers.</li>
130+
<li><strong>Unsubscription</strong>: Unsubscribe from the event in <code>Dispose</code> to prevent memory leaks.</li>
131+
</ul>
132+
</blockquote>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"Title": "Single Source of Truth",
3+
"Slug": "component-single-source-of-truth",
4+
"Description": "Implementing single source of truth pattern."
5+
}
7.54 KB
Loading

0 commit comments

Comments
 (0)