[Canonical page](<https://docs.dhtmlx.com/planning-engine/net/live-projects/previewing-changes/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Previewing Changes

This guide shows how to see what an edit would do before you commit it. It covers previewing a drag or a resize, keeping previews in sync with the project's baseline, checking the resource load a change would cause, and trying multi-step edits or a leveling run on a copy of the project.

## How previews work

A solved [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>) holds one **accepted baseline**: the dates from its last [`Solve`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) or [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>). Every preview starts from that baseline.

[`Project.Preview(changes, options)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-preview.md>) copies the baseline, applies a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/scheduling-changes.md>) to the copy and schedules it. It drops the copy when the call returns. The project itself doesn't change, so you can call `Preview` as often as the UI needs, for example on every mouse move during a drag.

You get back a regular [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md>):

- [`Updates`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#updates>) are relative to the baseline. They list the tasks the change would move and the tasks the change itself touched, the same way `Solve(null, …)` reports them after an [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>). An empty change set gives empty `Updates`.
- [`Conflicts`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#conflicts>), [`Cycles`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#cycles>) and [`Notes`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#notes>) describe the previewed state. A preview can show the cycle a new link would create, and the project never holds that cycle.
- [`BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#baselineid>) names the baseline the preview read.

`Preview` and `Apply` differ in two ways. A preview is all or nothing: if any operation in the change set is invalid, the whole call throws [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) for the first rejected operation. A preview also needs a clean, solved baseline. Before the first solve, and after any `Apply` until the next solve, it throws [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md>) with `Code == ProjectError.NoBaseline`.

For a change that takes more than one step, or for a `Level` run, use [`Project.Clone()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>) instead. A clone is copy-on-write: it costs O(1), and the two copies split only when one of them is edited. Edit, solve or level the clone, and the original stays as it was.

| You want to see | Use |
|---|---|
| The effect of one change set (a drag, a resize, a new link) | `Preview` |
| The load a change would put on resources | `Preview` with [`PreviewOptions.Profile`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md#profile>) |
| Several edits in a row, each solved | `Clone()`, then `Apply` + `Solve(null, …)` on the copy |
| What leveling would do | `Clone()`, then `Level` on the copy |

## Previewing an edit

Build the edit as a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/scheduling-changes.md>), the same object you'd pass to [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>), and pass it to [`Preview`](<https://docs.dhtmlx.com/planning-engine/net/api/project-preview.md>).

```csharp
using Plen;

static Moment Day(int day) =>
    Moment.FromYmdHm(2026, 1, day, 0, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("a") { StartDate = Day(1), Duration = 2.0 },
        new TaskData("b") { StartDate = Day(1), Duration = 1.0 },
    ],
    Links = [new LinkData { Id = "ab", Source = "a", Target = "b" }],
};
// All time is working time: plain day arithmetic, no weekends.
data.Config.UseCalendars = false;

var project = new Project();
var options = new SolveOptions();
var solved = project.Solve(data, options);

// The user drags task "b" to January 5.
var drag = new SchedulingChanges();
drag.Tasks.Update.Add(new TaskUpdate("b") { StartDate = Day(5) });

var preview = project.Preview(drag, new PreviewOptions());
foreach (var update in preview.Updates)
    Console.WriteLine($"task {update.Id}: {update.StartDate} .. {update.EndDate}");

// The project still holds the solved dates: "b" starts on January 3.
Console.WriteLine(ReferenceEquals(solved, project.Solve(null, options))); // True
```

The solve puts `b` right after `a`, on January 3. The preview prints `task b: 2026-01-05 00:00 .. 2026-01-06 00:00`. The project doesn't change: the next [`Solve(null, …)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) returns the cached result of the first solve, the same object.

When the user drops the task, send the same `SchedulingChanges` to `Apply` and then call `Solve(null, …)`. See [Editing a project](<https://docs.dhtmlx.com/planning-engine/net/live-projects/editing-a-project.md>).

[`PreviewOptions`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md>) has the same diagnostic switches as [`SolveOptions`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/solve-options.md>):

- [`Subtree`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md#subtree>) previews only one branch.
- [`Reasons`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md#reasons>) adds a [`Reason`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/reason.md>) to each update, so you can show why a task would move.
- [`Analytics`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md#analytics>) adds the critical path and slack of the previewed state.

Previews stay sparse: there's no [`IncludeUnchanged`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/solve-options.md#includeunchanged>) option. The full list is on the [`PreviewOptions` reference](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md>).

## Guarding with baseline tokens

A UI that previews in the background can run into a race. Another request applies an edit or reloads the data, and the preview now runs against a baseline the user never saw. Baseline tokens catch this.

Every `Solve` and `Level` result carries [`BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#baselineid>), an opaque token for the accepted baseline. Pass it back in [`PreviewOptions.BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md#baselineid>) (or [`SchedulingChanges.BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/scheduling-changes.md#baselineid>)). If the token no longer names the current baseline, `Preview` throws [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md>) with `Code == ProjectError.StaleBaseline` instead of returning dates for the wrong state.

```csharp
using Plen;

static Moment Day(int day) =>
    Moment.FromYmdHm(2026, 1, day, 0, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks = [new TaskData("a") { StartDate = Day(1), Duration = 2.0 }],
};
data.Config.UseCalendars = false;

var project = new Project();
var options = new SolveOptions();
var loaded = project.Solve(data, options);

// The UI keeps the token of the dates it shows.
var shown = loaded.BaselineId;

var resize = new SchedulingChanges();
resize.Tasks.Update.Add(new TaskUpdate("a") { Duration = 3.0 });

var guarded = new PreviewOptions { BaselineId = shown };
var preview = project.Preview(resize, guarded);
// A preview reports the baseline it read, so its token works for the next preview too.
Console.WriteLine(preview.BaselineId == shown); // True

// Meanwhile, another edit is committed.
project.Apply(resize);

// Until the next solve there's no baseline to preview against.
try
{
    project.Preview(resize, new PreviewOptions());
}
catch (ProjectException e) when (e.Code == ProjectError.NoBaseline)
{
    Console.WriteLine("no baseline until the next solve");
}

var latest = project.Solve(null, options);

// The old token no longer names the baseline.
try
{
    project.Preview(resize, guarded);
}
catch (ProjectException e) when (e.Code == ProjectError.StaleBaseline)
{
    // Refresh the view from `latest`, then preview with its token.
    project.Preview(resize, new PreviewOptions { BaselineId = latest.BaselineId });
}
```

A few rules for tokens:

- Take the token from the latest [`Solve`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) or [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) result. A preview result carries the token it read, so a chain of previews during one drag can reuse it.
- The token changes whenever the data changes: new data, an [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>), a solve that moves dates, leveling edits.
- Tokens are opaque strings. Don't parse them, store them across runs or compare them between processes. The same data can get a new token on a repeat run.
- [`Preview`](<https://docs.dhtmlx.com/planning-engine/net/api/project-preview.md>) checks `PreviewOptions.BaselineId` and `SchedulingChanges.BaselineId` separately. Leave both `null` to skip the check.

For how to handle `StaleBaseline` and the other project errors, see [Error handling](<https://docs.dhtmlx.com/planning-engine/net/error-handling.md>).

## Preview with load

Set [`PreviewOptions.Profile`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md#profile>) to a [`ProfileQuery`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/profile-query.md>) to see the resource load a change would cause. The result then has [`Profile`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#profile>), the same per-bucket load and capacity that [`LoadProfile`](<https://docs.dhtmlx.com/planning-engine/net/api/project-load-profile.md>) returns, but computed on the previewed dates. Use it to show a resource histogram while the user drags a task.

```csharp
using Plen;

static Moment Day(int day) =>
    Moment.FromYmdHm(2026, 1, day, 0, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("a") { StartDate = Day(1), Duration = 2.0 },
        new TaskData("b") { StartDate = Day(3), Duration = 1.0 },
    ],
    Resources = [new ResourceData { Id = "dev", Capacity = 1.0 }],
    Assignments =
    [
        new AssignmentData { Task = "a", Resource = "dev", Value = 1.0 },
        new AssignmentData { Task = "b", Resource = "dev", Value = 1.0 },
    ],
};
data.Config.UseCalendars = false;

var project = new Project();
project.Solve(data, new SolveOptions());

// Drag "b" one day earlier, onto the last day of "a".
var drag = new SchedulingChanges();
drag.Tasks.Update.Add(new TaskUpdate("b") { StartDate = Day(2) });

var options = new PreviewOptions
{
    Profile = new ProfileQuery(Day(1), Day(5)) { Tasks = ProfileTasks.Over },
};
var preview = project.Preview(drag, options);

if (preview.Profile is { } profile)
{
    foreach (var resource in profile.Resources)
    {
        foreach (var bucket in resource.Buckets.Where(b => b.Load > b.Capacity))
        {
            var tasks = string.Join(", ", bucket.Tasks ?? []);
            Console.WriteLine(
                $"{resource.Resource} overloaded at {bucket.Start}: {bucket.Load} of {bucket.Capacity} ({tasks})");
        }
    }
}
```

Both tasks need `dev` on January 2, so that bucket's load goes over its capacity of 1. With [`ProfileTasks.Over`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/profile-tasks.md>), each overloaded bucket also lists the tasks that share it.

The query covers buckets that start in `[From, To)`. By default it reports every resource, and the bucket size ([`Basis`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/basis.md>)) comes from [`Config.DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>). Narrow it with `Resources`, or set `Basis`. The query has no `BaselineId` of its own: the preview checks [`PreviewOptions.BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md#baselineid>). Every preview also reports [`Overallocations`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#overallocations>) when the project has assignments, so you can skip the profile if you need only the overloaded windows. See [Resource load](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/resource-load.md>) and the [`ProfileQuery` reference](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/profile-query.md>).

## Cloning a project

[`Preview`](<https://docs.dhtmlx.com/planning-engine/net/api/project-preview.md>) takes one change set. To see the result of several edits in a row, each one solved before the next, clone the project and work on the copy.

```csharp
using Plen;

static Moment Day(int day) =>
    Moment.FromYmdHm(2026, 1, day, 0, 0) ?? throw new InvalidOperationException("invalid date");

static SchedulingChanges Resize(Id task, double duration)
{
    var changes = new SchedulingChanges();
    changes.Tasks.Update.Add(new TaskUpdate(task) { Duration = duration });
    return changes;
}

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("a") { StartDate = Day(1), Duration = 2.0 },
        new TaskData("b") { StartDate = Day(1), Duration = 1.0 },
    ],
    Links = [new LinkData { Id = "ab", Source = "a", Target = "b" }],
};
data.Config.UseCalendars = false;

var options = new SolveOptions();
var project = new Project();
project.Solve(data, options);

// A cheap copy: it shares state with `project` until one of them is edited.
var draft = project.Clone();
draft.Apply(Resize("a", 4.0));
draft.Solve(null, options);
draft.Apply(Resize("b", 3.0));
var outcome = draft.Solve(null, options);

foreach (var update in outcome.Updates)
    Console.WriteLine($"draft task {update.Id}: {update.StartDate} .. {update.EndDate}");

// The original is untouched and still previews against its own baseline.
Console.WriteLine(project.Preview(new SchedulingChanges(), new PreviewOptions()).Updates.Count); // 0

// The user accepts the draft: keep it as the project.
project = draft;
```

Edits on one copy never affect the other. The copy is a full [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>) with its own baseline, cache and [`BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#baselineid>), so you can preview, level, evaluate or keep editing it. To accept the draft, replace the original with it, or replay the same changes on the original with [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>). To discard it, drop the reference. Nothing needs disposing.

Clone before [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) too. `Level` accepts the leveled dates as the project's new baseline, so running it on a copy is how you show the user a leveling proposal before you commit it:

```csharp
using Plen;

static void ShowLevelingProposal(Project project)
{
    var draft = project.Clone();
    var leveled = draft.Level(new LevelingOptions());
    foreach (var update in leveled.Schedule.Updates)
        Console.WriteLine($"task {update.Id} delay {update.LevelingDelay}");
}
```

The step-by-step version is in [Preview leveling](<https://docs.dhtmlx.com/planning-engine/net/how-to/preview-leveling.md>). To compare several alternative change sets with scores, use [`Evaluate`](<https://docs.dhtmlx.com/planning-engine/net/api/project-evaluate.md>) instead, which works on disposable copies internally. See [Evaluating alternatives](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/evaluating-alternatives.md>).

Clones also help with threads. A `Project` isn't thread-safe, and even [`Clone()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md#clone>) writes copy-on-write flags on the source. To work from several threads, make one `Clone()` per thread on a single thread first, or guard a shared project with a `lock`. See [Share a project between threads](<https://docs.dhtmlx.com/planning-engine/net/how-to/share-a-project-between-threads.md>).
