Skip to content
This repository has been archived by the owner on Dec 22, 2023. It is now read-only.

Documents

jacobslusser edited this page Jun 2, 2015 · 3 revisions

This is an advanced topic.

Scintilla has limited support for document-control separation, allowing more than one Scintilla control to access the same document (e.g. splitter windows), one control to access multiple documents (tabs), or for loading a document outside of a Scintilla control.

To work with documents requires a firm understanding of how document reference counting works. As has been said before, one of the project goals of the new ScintillaNET was to not shield users from the core Scintilla API and working with Document instances, ILoader instances, and reference counting is a prime example of that. It requires diligent programming to prevent memory leaks.

Understanding Document Reference Counts

One of the things we must get straight right away is to understand that every Document in Scintilla contains a reference count. When a new document is created the reference count is 1. When a document is associated with a Scintilla control or the Scintilla.AddRefDocument method is used, that reference count increases. When a document is removed from a Scintilla control or the Scintilla.ReleaseDocument method is used, that count is decreased. When the reference count reaches 0 the document will be released and the memory freed. Conversely, if the document count never reaches 0 memory leaks will occur.

So making sure a document reference count reaches 0 is good, however, allowing that to happen when the document is currently in use by a Scintilla control is bad. Don't do that. The universe will implode.

With that out of the way...

Multiple Views of one Document

To share the same document between two Scintilla controls, set their Document properties to the same value:

scintilla2.Document = scintilla1.Document;

The scintilla2 control will now show the same contents as the scintilla1 control (and vice versa). To break the connection we can force scintilla2 to drop the current document reference and create a new one by setting the Document property to Document.Empty:

scintilla2.Document = Document.Empty;

This type of document sharing is relatively free from having to track reference counts. Each time a document is associated with more than one Scintilla control, the document reference count is increased. Each time it is dissociated that reference count is decreased--including when a Scintilla control is disposed. If that was the last association to that document, the reference count of the document would reach 0 and the memory freed.

NOTE: The Document.Empty constant is not an empty document; it is the absence of one (i.e. null).

Multiple Documents for one View

Most modern text editors and IDEs support the ability to have multiple tabs open at once. One way to accomplish that would be to have multiple Scintilla controls, each with their own document. To conserve resources, however, you could choose to have only one Scintilla control and select multiple documents in and out of it. Your tabs would then simply be a way to switch between those documents, not display separate controls. This is how Notepad++ handles multiple documents.

As previously explained, each time a Document is unassociated with a Scintilla control its reference count decreases by one. So, if we want to select a new blank document into a Scintilla control, but not delete the current one, we need to increase the reference count of the current document before we select the new one:

private void NewDocument()
{
    var document = scintilla.Document;
    scintilla.AddRefDocument(document);

    // Replace the current document with a new one
    scintilla.Document = Document.Empty;
}

Said another way, calling AddRefDocument will increase the current document reference count from 1 to 2. When the Document property is set, the current document is unassociated with the control and its count is decreased from 2 to 1. Had we not prematurely increased the reference count, it would have gone from 1 to 0 and the document would have been deleted and not available for us to select back into the Scintilla control at a later time.

To switch the current document with an existing one (i.e. switch tabs) works almost the same way. We first increase the current document reference count from 1 to 2 so that when its unassociated with the Scintilla control it would drop from 2 to 1 and not be deleted. When the next (existing) document is selected into the Scintilla control its count will increase from 1 to 2, so we call ReleaseDocument after making the association to drop it back to 1 and make Scintilla the sole owner:

private void SwitchDocument(Document nextDocument)
{
    var prevDocument = scintilla.Document;
    scintilla.AddRefDocument(prevDocument);

    // Replace the current document and make Scintilla the owner
    scintilla.Document = nextDocument;
    scintilla.ReleaseDocument(nextDocument);
}

At any time you can use the AddRefDocument and ReleaseDocument methods to make these kind of adjustments so long as you're mindful of the reference count.

Background Loading

A Document can be loaded in a background thread using an ILoader instance. The ILoader interface contains only three methods: AddData, ConvertToDocument, and Release. To add text to the document call AddData. Once done, call ConvertToDocument to get the Document handle.

I find the ConvertToDocument method to have a misleading name. Internally an ILoader instance contains a new Document instance. The ConvertToDocument method simply returns that internal instance. No 'conversion' is taking place.

Once it's understood that the ILoader is a wrapper around a Document it should be obvious that we need to keep track of its internal document reference count just as we would any other document. When the ILoader is created, the internal document has a reference count of 1 (as do all new documents). If for any reason we need to get rid of this document because there was an error loading in the data or we want to cancel the process we would need to decrease that internal document reference count or incur a memory leak.

That is the whole purpose for the ILoader.Release method--to decrease the reference count of the internal document. That also explains why we shouldn't call Release on the ILoader once we've successfully called ConvertToDocument. That would decrease the reference count of the document we just got access to.

This is best explained with an example. The method below will use the ILoader instance specified to load characters data from a file at the specified path and return the completed document:

private async Task<Document> LoadFileAsync(ILoader loader, string path, CancellationToken cancellationToken)
{
    try
    {
        using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true))
        using (var reader = new StreamReader(file))
        {
            var count = 0;
            var buffer = new char[4096];
            while ((count = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
            {
                // Check for cancellation
                cancellationToken.ThrowIfCancellationRequested();

                // Add the data to the document
                if (!loader.AddData(buffer, count))
                    throw new IOException("The data could not be added to the loader.");
            }

            return loader.ConvertToDocument();
        }
    }
    catch
    {
        loader.Release();
        throw;
    }
}

Data is added through the AddData method. This is done on a background thread and is the whole point of using the ILoader. If AddData returns false it means Scintilla encountered an error (out of memory) and we should stop loading. If that or any other error occurs--including cancellation--we're careful to make sure we call ILoader.Release in our catch block to drop the reference count of the internal document from 1 to 0 so that it will be deleted.

To use our new LoadFileAsync method, we might do something like this:

private async void button_Click(object sender, EventArgs e)
{
    try
    {
        var loader = scintilla.CreateLoader(256);
        if (loader == null)
            throw new ApplicationException("Unable to create loader.");

        var cts = new CancellationTokenSource();
        var document = await LoadFileAsync(loader, @"your_file_path.txt", cts.Token);
        scintilla.Document = document;

        // Every document starts with a reference count of 1. Assigning it to Scintilla increased that to 2.
        // To let Scintilla control the life of the document, we'll drop it back down to 1.
        scintilla.ReleaseDocument(document);
    }
    catch (OperationCanceledException)
    {
    }
    catch(Exception)
    {
        MessageBox.Show(this, "There was an error loading the file.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

An ILoader is created by calling Scintilla.CreateLoader. The length argument of 256 is arbitrary and is meant to be a hint to Scintilla on how much memory it should allocate. If the loader exceeds the initial allocation, it will automatically allocate more so you don't need to be exact.

As noted in the code comments (and ad nauseam above), we must decrease the document reference count once selected into the Scintilla control because the document is born with a reference count of 1. That increased from 1 to 2 when associated with the control. If we're not sure we're going to take ownership back any time soon, it's probably best to make Scintilla the sole owner so we drop it back from 2 to 1.

NOTE: While AddData is meant to be called from a background thread, Scintilla.CreateLoader, Scintilla.Document, Scintilla.ReleaseDocument, etc... are not. Any interaction with Scintilla should always be done on the UI thread.