Skip to content

Commit 42bb6ea

Browse files
authored
Merge pull request #1902 from riganti/feature/timer
Timer control added
2 parents afbcd6b + 5c37ea9 commit 42bb6ea

File tree

12 files changed

+468
-3
lines changed

12 files changed

+468
-3
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using DotVVM.Framework.Binding.Expressions;
5+
using DotVVM.Framework.Binding;
6+
using DotVVM.Framework.Hosting;
7+
8+
namespace DotVVM.Framework.Controls
9+
{
10+
/// <summary>
11+
/// An invisible control that periodically invokes a command.
12+
/// </summary>
13+
public class Timer : DotvvmControl
14+
{
15+
/// <summary>
16+
/// Gets or sets the command binding that will be invoked on every tick.
17+
/// </summary>
18+
[MarkupOptions(AllowHardCodedValue = false, Required = true)]
19+
public ICommandBinding Command
20+
{
21+
get { return (ICommandBinding)GetValue(CommandProperty)!; }
22+
set { SetValue(CommandProperty, value); }
23+
}
24+
public static readonly DotvvmProperty CommandProperty
25+
= DotvvmProperty.Register<ICommandBinding, Timer>(c => c.Command, null);
26+
27+
/// <summary>
28+
/// Gets or sets the interval in milliseconds.
29+
/// </summary>
30+
[MarkupOptions(AllowBinding = false, Required = true)]
31+
public int Interval
32+
{
33+
get { return (int)GetValue(IntervalProperty)!; }
34+
set { SetValue(IntervalProperty, value); }
35+
}
36+
public static readonly DotvvmProperty IntervalProperty
37+
= DotvvmProperty.Register<int, Timer>(c => c.Interval, 30000);
38+
39+
/// <summary>
40+
/// Gets or sets whether the timer is enabled.
41+
/// </summary>
42+
public bool Enabled
43+
{
44+
get { return (bool)GetValue(EnabledProperty)!; }
45+
set { SetValue(EnabledProperty, value); }
46+
}
47+
public static readonly DotvvmProperty EnabledProperty
48+
= DotvvmProperty.Register<bool, Timer>(c => c.Enabled, true);
49+
50+
public Timer()
51+
{
52+
SetValue(Validation.EnabledProperty, false);
53+
SetValue(PostBack.ConcurrencyProperty, PostbackConcurrencyMode.Queue);
54+
}
55+
56+
protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext context)
57+
{
58+
var group = new KnockoutBindingGroup();
59+
group.Add("command", KnockoutHelper.GenerateClientPostbackLambda("Command", Command, this));
60+
group.Add("interval", Interval.ToString());
61+
group.Add("enabled", this, EnabledProperty);
62+
writer.WriteKnockoutDataBindComment("dotvvm-timer", group.ToString());
63+
64+
base.RenderBeginTag(writer, context);
65+
}
66+
67+
protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext context)
68+
{
69+
base.RenderEndTag(writer, context);
70+
71+
writer.WriteKnockoutDataBindEndComment();
72+
}
73+
}
74+
}

src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import fileUpload from './file-upload'
1212
import jsComponents from './js-component'
1313
import modalDialog from './modal-dialog'
1414
import appendableDataPager from './appendable-data-pager'
15+
import timer from './timer'
1516

1617
type KnockoutHandlerDictionary = {
1718
[name: string]: KnockoutBindingHandler
@@ -30,7 +31,8 @@ const allHandlers: KnockoutHandlerDictionary = {
3031
...fileUpload,
3132
...jsComponents,
3233
...modalDialog,
33-
...appendableDataPager
34+
...appendableDataPager,
35+
...timer
3436
}
3537

3638
export default allHandlers
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
type TimerProps = {
2+
interval: number,
3+
enabled: KnockoutObservable<boolean>,
4+
command: () => Promise<DotvvmAfterPostBackEventArgs>
5+
}
6+
7+
ko.virtualElements.allowedBindings["dotvvm-timer"] = true;
8+
9+
export default {
10+
"dotvvm-timer": {
11+
init: (element: HTMLElement, valueAccessor: () => TimerProps) => {
12+
const prop = valueAccessor();
13+
let timer: number | null = null;
14+
15+
const observable = ko.isObservable(prop.enabled) ? prop.enabled : ko.pureComputed(() => ko.unwrap(valueAccessor().enabled));
16+
const subscription = observable.subscribe(newValue => createOrDestroyTimer(newValue));
17+
createOrDestroyTimer(ko.unwrap(prop.enabled));
18+
19+
function createOrDestroyTimer(enabled: boolean) {
20+
if (enabled) {
21+
if (timer) {
22+
window.clearTimeout(timer);
23+
}
24+
25+
const callback = async () => {
26+
try {
27+
await prop.command.bind(element)();
28+
} catch (err) {
29+
dotvvm.log.logError("postback", err);
30+
}
31+
timer = window.setTimeout(callback, prop.interval);
32+
};
33+
timer = window.setTimeout(callback, prop.interval);
34+
35+
} else if (timer) {
36+
window.clearTimeout(timer);
37+
}
38+
};
39+
40+
ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
41+
subscription.dispose();
42+
createOrDestroyTimer(false);
43+
});
44+
}
45+
}
46+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using DotVVM.Framework.ViewModel;
8+
using DotVVM.Framework.Hosting;
9+
10+
namespace DotVVM.Samples.Common.ViewModels.ControlSamples.Timer
11+
{
12+
public class LongCommandViewModel : DotvvmViewModelBase
13+
{
14+
public int Value { get; set; }
15+
16+
public async Task LongCommand()
17+
{
18+
await Task.Delay(3000);
19+
Value++;
20+
}
21+
}
22+
}
23+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using DotVVM.Framework.ViewModel;
7+
using DotVVM.Framework.Hosting;
8+
9+
namespace DotVVM.Samples.Common.ViewModels.ControlSamples.Timer
10+
{
11+
public class RemovalViewModel : DotvvmViewModelBase
12+
{
13+
public bool Disabled { get; set; } = false;
14+
15+
public int Value { get; set; }
16+
17+
public bool IsRemoved { get; set; } = false;
18+
19+
}
20+
}
21+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using DotVVM.Framework.ViewModel;
7+
using DotVVM.Framework.Hosting;
8+
9+
namespace DotVVM.Samples.Common.ViewModels.ControlSamples.Timer
10+
{
11+
public class TimerViewModel : DotvvmViewModelBase
12+
{
13+
public int Value1 { get; set; }
14+
public int Value2 { get; set; }
15+
public int Value3 { get; set; }
16+
17+
public bool Enabled1 { get; set; } = true;
18+
public bool Enabled2 { get; set; }
19+
}
20+
}
21+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@viewModel DotVVM.Samples.Common.ViewModels.ControlSamples.Timer.LongCommandViewModel, DotVVM.Samples.Common
2+
3+
<!DOCTYPE html>
4+
5+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
6+
<head>
7+
<meta charset="utf-8" />
8+
<title></title>
9+
</head>
10+
<body>
11+
12+
<h1>Timer with long command</h1>
13+
14+
<p class="result">{{value: Value}}</p>
15+
<dot:Timer Command="{command: LongCommand()}" Interval="1000" />
16+
17+
</body>
18+
</html>
19+
20+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@viewModel DotVVM.Samples.Common.ViewModels.ControlSamples.Timer.RemovalViewModel, DotVVM.Samples.Common
2+
3+
<!DOCTYPE html>
4+
5+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
6+
<head>
7+
<meta charset="utf-8" />
8+
<title></title>
9+
</head>
10+
<body>
11+
<h1>Making sure that removing timer disposes the callback</h1>
12+
13+
<p class="result">{{value: Value}}</p>
14+
<dot:CheckBox Text="Disabled" data-ui="disabled" Checked="{value: Disabled}" />
15+
16+
<dot:Timer Command="{staticCommand: Value = Value + 1}" Interval="1000" Enabled="{value: !Disabled}"
17+
IncludeInPage="{value: !IsRemoved}"/>
18+
<dot:Button Text="Remove timer from DOM" data-ui="remove" Click="{staticCommand: IsRemoved = true}" />
19+
20+
</body>
21+
</html>
22+
23+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@viewModel DotVVM.Samples.Common.ViewModels.ControlSamples.Timer.TimerViewModel, DotVVM.Samples.Common
2+
3+
<!DOCTYPE html>
4+
5+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
6+
<head>
7+
<meta charset="utf-8" />
8+
<title></title>
9+
</head>
10+
<body>
11+
12+
<h1>Timer</h1>
13+
14+
<div>
15+
<h2>Timer 1 - enabled from the start</h2>
16+
<p>
17+
Value: <span data-ui="value1">{{value: Value1}}</span>
18+
</p>
19+
<p>
20+
<dot:CheckBox data-ui="enabled1" Checked="{value: Enabled1}" Text="Timer enabled" />
21+
</p>
22+
23+
<dot:Timer Interval="1000" Command="{command: Value1 = Value1 + 1}" Enabled="{value: Enabled1}" />
24+
</div>
25+
26+
<div>
27+
<h2>Timer 2 - disabled from the start</h2>
28+
<p>
29+
Value: <span data-ui="value2">{{value: Value2}}</span>
30+
</p>
31+
<p>
32+
<dot:CheckBox data-ui="enabled2" Checked="{value: Enabled2}" Text="Timer enabled" />
33+
</p>
34+
35+
<dot:Timer Interval="2000" Command="{command: Value2 = Value2 + 1}" Enabled="{value: Enabled2}" />
36+
</div>
37+
38+
<div>
39+
<h2>Timer 3 - without Enabled property</h2>
40+
<p>
41+
Value: <span data-ui="value3">{{value: Value3}}</span>
42+
</p>
43+
44+
<dot:Timer Interval="3000" Command="{command: Value3 = Value3 + 1}" />
45+
</div>
46+
47+
</body>
48+
</html>
49+
50+

src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)