PROWAREtech
Blazor: Timer Example - Analog Clock
This code is compatible with .NET Core 3.1, .NET 5, .NET 6 and .NET 8 (Blazor Server).
Here is an example that uses the timer (System.Threading.Timer
) to set an analog clock. It runs as server-side Blazor "SignalR" code. For a client-side WASM version, see this article.
All measurements are based off of percentages so it will scale to practically any size.
Here is the clock specific CSS code that should be added to the Blazor SignalR app.
.clock-container {
background-color: midnightblue;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 75vmin;
}
.clock {
position: relative;
overflow: hidden;
background-color: inherit;
height: 70vmin;
width: 70vmin;
border-radius: 50%;
box-shadow: 0 -12px 12px rgba(255,255,255,.1),
inset 0 -12px 12px rgba(255,255,255,.1),
0 12px 12px rgba(0,0,0,.1),
inset 0 12px 12px rgba(0,0,0,.1);
}
.clock div {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: transparent;
}
.clock div div {
left: 50%;
width: 0;
}
.clock span {
position: absolute;
font-family: Arial;
font-size: 5vmin;
font-weight: bold;
color: lime;
}
.clock .h12 {
left: 50%;
top: 3%;
transform: translateX(-50%);
}
.clock .h12::before {
content: "12";
}
.clock .h3 {
left: 97%;
top: 50%;
transform: translate(-100%, -50%);
}
.clock .h3::before {
content: "3";
}
.clock .h6 {
left: 50%;
top: 97%;
transform: translate(-50%, -100%);
}
.clock .h6::before {
content: "6";
}
.clock .h9 {
left: 3%;
top: 50%;
transform: translateY(-50%);
}
.clock .h9::before {
content: "9";
}
.clock .ctr {
width: 3%;
height: 3%;
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: wheat;
}
.clock .hour div {
top: 20%;
height: 30%;
border: 2px solid wheat;
margin-left: -2px;
}
.clock .minute div {
top: 10%;
height: 40%;
border: 2px solid wheat;
margin-left: -2px;
}
.clock .second div {
top: 5%;
height: 65%;
border: 1px solid red;
margin-left: -1px;
}
Here is the Index.razor page that uses the Timer
class. It needs to call StateHasChanged()
because there is no user-fired event.
@page "/"
<div class="clock-container">
<div class="clock">
<span class="h12"></span>
<span class="h3"></span>
<span class="h6"></span>
<span class="h9"></span>
<div class="hour" style="transform:rotate(@(hr)deg);"><div></div></div>
<div class="minute" style="transform:rotate(@(min)deg);"><div></div></div>
<div class="second" style="transform:rotate(@(sec)deg);"><div></div></div>
<span class="ctr"></span>
</div>
</div>
@code {
System.Threading.Timer timer;
double hr, min, sec;
// NOTE: this math can be simplified!!!
private void SetClock(object stateInfo)
{
var time = DateTime.Now;
hr = 360.0 * time.Hour / 12 + 30.0 * time.Minute / 60.0;
min = 360.0 * time.Minute / 60 + 6.0 * time.Second / 60.0;
sec = 360.0 * time.Second / 60 + 6.0 * time.Millisecond / 1000.0;
InvokeAsync(() => StateHasChanged()); // MUST CALL StateHasChanged() BECAUSE THIS IS TRIGGERED BY A TIMER INSTEAD OF A USER EVENT
}
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
SetClock(null);
timer = new System.Threading.Timer(SetClock, new System.Threading.AutoResetEvent(false), 1000, 1000);
}
}
.NET 8 Server App
Make sure the .NET 8 project is using the "Server" interactive render mode, as pictured here:
Here is the Home.razor page that uses the Timer
class. It needs to call StateHasChanged()
because there is no user-fired event. Notice the addition of @rendermode
.
@page "/"
@rendermode InteractiveServer
<div class="clock-container">
<div class="clock">
<span class="h12"></span>
<span class="h3"></span>
<span class="h6"></span>
<span class="h9"></span>
<div class="hour" style="transform:rotate(@(hr)deg);"><div></div></div>
<div class="minute" style="transform:rotate(@(min)deg);"><div></div></div>
<div class="second" style="transform:rotate(@(sec)deg);"><div></div></div>
<span class="ctr"></span>
</div>
</div>
@code {
System.Threading.Timer timer;
double hr, min, sec;
// NOTE: this math can be simplified!!!
private void SetClock(object? stateInfo)
{
var time = DateTime.Now;
hr = 360.0 * time.Hour / 12 + 30.0 * time.Minute / 60.0;
min = 360.0 * time.Minute / 60 + 6.0 * time.Second / 60.0;
sec = 360.0 * time.Second / 60 + 6.0 * time.Millisecond / 1000.0;
InvokeAsync(() => StateHasChanged()); // MUST CALL StateHasChanged() BECAUSE THIS IS TRIGGERED BY A TIMER INSTEAD OF A USER EVENT
}
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
SetClock(null);
timer = new System.Threading.Timer(SetClock, new System.Threading.AutoResetEvent(false), 1000, 1000);
}
}