Skip to content

Commit

Permalink
Timer control added
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasherceg committed Jan 3, 2025
1 parent 5b54b96 commit a774a05
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 3 deletions.
74 changes: 74 additions & 0 deletions src/Framework/Framework/Controls/Timer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Text;
using DotVVM.Framework.Binding.Expressions;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Hosting;

namespace DotVVM.Framework.Controls
{
/// <summary>
/// An invisible control that periodically invokes a command.
/// </summary>
public class Timer : DotvvmControl
{
/// <summary>
/// Gets or sets the command binding that will be invoked on every tick.
/// </summary>
[MarkupOptions(AllowHardCodedValue = false, Required = true)]
public ICommandBinding Command
{
get { return (ICommandBinding)GetValue(CommandProperty); }

Check failure on line 21 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Release)

Converting null literal or possible null value to non-nullable type.

Check failure on line 21 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Release)

Possible null reference return.

Check failure on line 21 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Release)

Converting null literal or possible null value to non-nullable type.

Check failure on line 21 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Release)

Possible null reference return.

Check failure on line 21 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Debug)

Converting null literal or possible null value to non-nullable type.

Check failure on line 21 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Debug)

Possible null reference return.

Check failure on line 21 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Debug)

Converting null literal or possible null value to non-nullable type.

Check failure on line 21 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Debug)

Possible null reference return.
set { SetValue(CommandProperty, value); }
}
public static readonly DotvvmProperty CommandProperty
= DotvvmProperty.Register<ICommandBinding, Timer>(c => c.Command, null);

/// <summary>
/// Gets or sets the interval in milliseconds.
/// </summary>
[MarkupOptions(AllowBinding = false)]
public int Interval
{
get { return (int)GetValue(IntervalProperty); }

Check failure on line 33 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Release)

Unboxing a possibly null value.

Check failure on line 33 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Release)

Unboxing a possibly null value.

Check failure on line 33 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Debug)

Unboxing a possibly null value.

Check failure on line 33 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Debug)

Unboxing a possibly null value.
set { SetValue(IntervalProperty, value); }
}
public static readonly DotvvmProperty IntervalProperty
= DotvvmProperty.Register<int, Timer>(c => c.Interval, 30000);

/// <summary>
/// Gets or sets whether the timer is enabled.
/// </summary>
public bool Enabled
{
get { return (bool)GetValue(EnabledProperty); }

Check failure on line 44 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Release)

Unboxing a possibly null value.

Check failure on line 44 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Release)

Unboxing a possibly null value.

Check failure on line 44 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Debug)

Unboxing a possibly null value.

Check failure on line 44 in src/Framework/Framework/Controls/Timer.cs

View workflow job for this annotation

GitHub Actions / Build published projects without warnings (Debug)

Unboxing a possibly null value.
set { SetValue(EnabledProperty, value); }
}
public static readonly DotvvmProperty EnabledProperty
= DotvvmProperty.Register<bool, Timer>(c => c.Enabled, true);

public Timer()
{
SetValue(Validation.EnabledProperty, false);
SetValue(PostBack.ConcurrencyProperty, PostbackConcurrencyMode.Queue);
}

protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext context)
{
var group = new KnockoutBindingGroup();
group.Add("command", KnockoutHelper.GenerateClientPostbackLambda("Command", Command, this));
group.Add("interval", Interval.ToString());
group.Add("enabled", this, EnabledProperty);
writer.WriteKnockoutDataBindComment("timer", group.ToString());

base.RenderBeginTag(writer, context);
}

protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext context)
{
base.RenderEndTag(writer, context);

writer.WriteKnockoutDataBindEndComment();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import fileUpload from './file-upload'
import jsComponents from './js-component'
import modalDialog from './modal-dialog'
import appendableDataPager from './appendable-data-pager'
import timer from './timer'

type KnockoutHandlerDictionary = {
[name: string]: KnockoutBindingHandler
Expand All @@ -30,7 +31,8 @@ const allHandlers: KnockoutHandlerDictionary = {
...fileUpload,
...jsComponents,
...modalDialog,
...appendableDataPager
...appendableDataPager,
...timer
}

export default allHandlers
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
type TimerProps = {
interval: number,
enabled: KnockoutObservable<boolean>,
command: () => Promise<DotvvmAfterPostBackEventArgs>
}

ko.virtualElements.allowedBindings["timer"] = true;

export default {
"timer": {
init: (element: HTMLElement, valueAccessor: () => TimerProps) => {
const prop = valueAccessor();
let timer: number | null = null;

if (ko.isObservable(prop.enabled)) {
prop.enabled.subscribe(newValue => createOrDestroyTimer(newValue));
}
createOrDestroyTimer(ko.unwrap(prop.enabled));

function createOrDestroyTimer(enabled: boolean) {
if (enabled) {
if (timer) {
window.clearInterval(timer);
}

timer = window.setInterval(() => {
prop.command.bind(element)();
}, prop.interval);

} else if (timer) {
window.clearInterval(timer);
}
};

ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
if (timer) {
window.clearInterval(timer);
}
});
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DotVVM.Framework.ViewModel;
using DotVVM.Framework.Hosting;

namespace DotVVM.Samples.Common.ViewModels.ControlSamples.Timer
{
public class TimerViewModel : DotvvmViewModelBase
{
public int Value1 { get; set; }
public int Value2 { get; set; }
public int Value3 { get; set; }

public bool Enabled1 { get; set; } = true;
public bool Enabled2 { get; set; }
}
}

50 changes: 50 additions & 0 deletions src/Samples/Common/Views/ControlSamples/Timer/Timer.dothtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@viewModel DotVVM.Samples.Common.ViewModels.ControlSamples.Timer.TimerViewModel, DotVVM.Samples.Common

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>

<h1>Timer</h1>

<div>
<h2>Timer 1 - enabled from the start</h2>
<p>
Value: <span data-ui="value1">{{value: Value1}}</span>
</p>
<p>
<dot:CheckBox data-ui="enabled1" Checked="{value: Enabled1}" Text="Timer enabled" />
</p>

<dot:Timer Interval="1000" Command="{command: Value1 = Value1 + 1}" Enabled="{value: Enabled1}" />
</div>

<div>
<h2>Timer 2 - disabled from the start</h2>
<p>
Value: <span data-ui="value2">{{value: Value2}}</span>
</p>
<p>
<dot:CheckBox data-ui="enabled2" Checked="{value: Enabled2}" Text="Timer enabled" />
</p>

<dot:Timer Interval="2000" Command="{command: Value2 = Value2 + 1}" Enabled="{value: Enabled2}" />
</div>

<div>
<h2>Timer 3 - without Enabled property</h2>
<p>
Value: <span data-ui="value3">{{value: Value3}}</span>
</p>

<dot:Timer Interval="3000" Command="{command: Value3 = Value3 + 1}" />
</div>

</body>
</html>


4 changes: 2 additions & 2 deletions src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 102 additions & 0 deletions src/Samples/Tests/Tests/Control/TimerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using DotVVM.Samples.Tests.Base;
using Riganti.Selenium.Core;
using DotVVM.Testing.Abstractions;
using Xunit;
using Xunit.Abstractions;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium;
using Riganti.Selenium.Core.Abstractions;

namespace DotVVM.Samples.Tests.Control
{
public class TimerTests : AppSeleniumTest
{
public TimerTests(ITestOutputHelper output) : base(output)
{
}

[Fact]
public void Control_Timer_Timer_Timer1()
{
RunInAllBrowsers(browser => {
browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_Timer_Timer);

var value = browser.Single("[data-ui=value1]");

// ensure the first timer is running
Assert.True(EqualsWithTolerance(0, int.Parse(value.GetInnerText()), 1));
browser.Wait(3000);
Assert.True(EqualsWithTolerance(3, int.Parse(value.GetInnerText()), 1));
browser.Wait(3000);
Assert.True(EqualsWithTolerance(6, int.Parse(value.GetInnerText()), 1));

// stop the first timer
browser.Single("[data-ui=enabled1]").Click();
browser.Wait(3000);
Assert.True(EqualsWithTolerance(6, int.Parse(value.GetInnerText()), 1));
browser.Wait(3000);
Assert.True(EqualsWithTolerance(6, int.Parse(value.GetInnerText()), 1));

// restart the timer
browser.Single("[data-ui=enabled1]").Click();
browser.Wait(3000);
Assert.True(EqualsWithTolerance(9, int.Parse(value.GetInnerText()), 1));
browser.Wait(3000);
Assert.True(EqualsWithTolerance(12, int.Parse(value.GetInnerText()), 1));
});
}

[Fact]
public void Control_Timer_Timer_Timer2()
{
RunInAllBrowsers(browser => {
browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_Timer_Timer);

var value = browser.Single("[data-ui=value2]");

// ensure the timer is not running
Assert.True(EqualsWithTolerance(0, int.Parse(value.GetInnerText()), 1));
browser.Wait(3000);
Assert.True(EqualsWithTolerance(0, int.Parse(value.GetInnerText()), 1));
browser.Wait(3000);
Assert.True(EqualsWithTolerance(0, int.Parse(value.GetInnerText()), 1));

// start the second timer
browser.Single("[data-ui=enabled2]").Click();
browser.Wait(4000);
Assert.True(EqualsWithTolerance(2, int.Parse(value.GetInnerText()), 1));
browser.Wait(4000);
Assert.True(EqualsWithTolerance(4, int.Parse(value.GetInnerText()), 1));

// stop the second timer
browser.Single("[data-ui=enabled2]").Click();
browser.Wait(4000);
Assert.True(EqualsWithTolerance(4, int.Parse(value.GetInnerText()), 1));
browser.Wait(4000);
Assert.True(EqualsWithTolerance(4, int.Parse(value.GetInnerText()), 1));
});
}


[Fact]
public void Control_Timer_Timer_Timer3()
{
RunInAllBrowsers(browser => {
browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_Timer_Timer);

var value = browser.Single("[data-ui=value3]");

// ensure the timer is running
Assert.True(EqualsWithTolerance(0, int.Parse(value.GetInnerText()), 1));
browser.Wait(3000);
Assert.True(EqualsWithTolerance(1, int.Parse(value.GetInnerText()), 1));
browser.Wait(3000);
Assert.True(EqualsWithTolerance(2, int.Parse(value.GetInnerText()), 1));
});
}

private static bool EqualsWithTolerance(int expected, int actual, int tolerance)
=> Math.Abs(expected - actual) <= tolerance;
}
}

0 comments on commit a774a05

Please sign in to comment.