Return DataTable

343 views
Skip to first unread message

Hagai David

unread,
Mar 22, 2022, 1:22:02 PM3/22/22
to Excel-DNA
Hey, 
I'm trying return a DataTable object but I'm getting an error of N/A, I tracked the code the object indeed trying get into the excel but it stuck because the platfrom not supporting it I assume ? 
Does someone tackled into it ? 
If I want return a DataTable object how can I ? 
I want present in the Excel a table I have been created into the code.
I though maybe assist the in example of Govert of returning a table,  and then edit the values inside the cells in adjustment to the DataTable...
There is easy way doing that ? (an option return an DataTable) 
Thanks alot,
Hagai

Govert van Drimmelen

unread,
Mar 22, 2022, 4:03:56 PM3/22/22
to Excel-DNA
Hi Hagai,

Best is probably to return an object[,] array from your function.
The conversion from a DataTable can be quite simple, like the code here: https://groups.google.com/g/exceldna/c/JskQEs4xL08/m/Iw3iJ6grBAAJ

That works really well with the new Excel Dynamic Arrays features (so you can easily =FILTER(...), =SORT(...) etc from there).
If you're not using a version of Excel that can do Dynamic Arrays it's a bigger problem to make something nice.

-Govert

Hagai David

unread,
Mar 22, 2022, 5:48:10 PM3/22/22
to Excel-DNA
Dear Govert,
That's right I saw this option in google in some soruce however I got into a problem. 
When I use this code and getting an object[,] array I send it from "ConnectData" file to "Functions" file.
1 issue) I can't return from "ConnectData" an object[,] (when I changing it to  return   object[,]   it get me warnning)
I wrote like this:
protected override object[,] ConnectData(Topic topic, IList<string> topicInfo, ref bool newValues)

2)In assuming I getting  over it and I get the object[,] variable in "Functions" file  now I want send it back to the Excel as a Table so I though maybe using your code here:
I though maybe getting the object[,] like the code I added beneath and then using your ArrayResizer example ( I linked one row above) and  returning the Table to the Excel worksheet. 

public static object [,] TimerNow()
{
    object x = XlCall.RTD("RTD_07.RtdServer", null, "NOW", number.ToString());
...
}
Much appreciated,
Hagai
ב-יום שלישי, 22 במרץ 2022 בשעה 22:03:56 UTC+2, gov...@icon.co.za כתב/ה:

Govert van Drimmelen

unread,
Mar 22, 2022, 5:57:58 PM3/22/22
to exce...@googlegroups.com

Hi Hagai,

 

Does your version of Excel support Dynamic Arrays?

 

If it does not (e.g. Excel 2019 or earlier) then you cannot safely return arrays from an RTD Server.

You also cannot mix the ArrayResizer with an RTD function.

You can optimize the implementation of the RTD server to do some kind of batching, but your RTD topics should return simple values only.

 

Also, when you are implementing the RTD server (so you are writing the RTD code like ConnectData) you have to follow the RTD specification exactly.

 

If your version Excel does support Dynamic Arrays, your RTD server should still return only a simple value for each topic, but you can safely make wrapper functions that return arrays.

 

I know that might not answer all your questions, but implementing an RTD server from scratch as is not so easy as there is very little documentation (besides what I have sent you in previous discussions).

 

-Govert

--
You received this message because you are subscribed to the Google Groups "Excel-DNA" group.
To unsubscribe from this group and stop receiving emails from it, send an email to exceldna+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/exceldna/5f559b17-5350-4298-932b-84e5848a091fn%40googlegroups.com.

Hagai David

unread,
Mar 23, 2022, 2:09:30 PM3/23/22
to Excel-DNA
Dear Govert,
First thank you so much !!! 
Well, unfortunately  my Excel doesn't support Dynamic Arrays =\.
Yes I understand what you'r saying, maybe I haven't been clear enought about what I am looking for: 
I created an Http Get request in the Serverstart which get some details from a server like: mount of tables I need to ask, ID's of the table, how time elapsed till I asking again, how time elapsed till I refresh to the Excel .
(I already filterred the Data I need and entered to my code and knows how to open timers for those demands).
Now after I got the former Data I know which Tables should I request and store in my "middle", I will always keep asking the functions from the server and Refresh the Table to the Excel (the excel that be refreshing is up to the user request in the Excel).
To your knowledge in ExcelDna can I find a solution for this case?
Much appreciated,
Hagai


ב-יום שלישי, 22 במרץ 2022 בשעה 23:57:58 UTC+2, Govert van Drimmelen כתב/ה:

Hagai David

unread,
Mar 24, 2022, 4:53:44 AM3/24/22
to Excel-DNA
someone has any idea to the situation to my case  :)?

ב-יום רביעי, 23 במרץ 2022 בשעה 20:09:30 UTC+2, ‪Hagai David‬‏ כתב/ה:

Govert van Drimmelen

unread,
Mar 24, 2022, 12:07:43 PM3/24/22
to exce...@googlegroups.com

Hi Hagai,

 

I would do something like the code below, which uses an internal object cache and returning a handle to the cell.

 

There are two UDF functions:

  • afFetchUrl – this is the async function that retrieves the information from the back-end. The result is stored internally, and a handle returned to the calling cell.
  • afGetSubstring – this is an example of a function that takes the handle and does some computation or extracts a part of it.

 

The access function (here afGetString) could return an array.

Since you do not have Dynamic Arrays available, you could use the ‘ArrayResizer’ hack to expand the calling array formula if required – some code here: https://groups.google.com/g/exceldna/c/WK6RfD5cym0/m/KaoZR6KPAwAJ

 

-Govert

 

 

 

using System;

using System.Collections.Generic;

using System.Net.Http;

using System.Threading.Tasks;

using ExcelDna.Integration;

 

namespace AsyncFetch

{

    public static class Functions

    {

        static HttpClient _client = new HttpClient();

 

        // This UDF will do some async work and when done will return a 'handle' as its result

        public static object afFetchUrl(string url)

        {

            var handleType = "FetchResult";     // Will be the name for our handles

            var args = new object[] { url };

 

            return HandleManager.GetHandle(handleType, args, async (args) =>

            {

                // Here is the async work that will return the target object referred to by the handle

                return await _client.GetStringAsync((string)args[0]);

            });

        }

 

        // This UDF can take the 'handle' as input, retrieve the the target object and use it to return some result

        public static object afGetSubstring(string handle, int start, int length)

        {

            if (HandleManager.TryGetObject(handle, out var response))

            {

                return ((string)response).Substring(start, length);

            }

            return ExcelError.ExcelErrorValue;

        }

    }

 

    class HandleManager

    {

        static int _handleIndex;

        static readonly Dictionary<string, HandleInfo> _objects = new();

 

        // Tries to get an existing handle for the given object type and parameters.

        // If there is no existing handle, creates a new handle with the target provided by evaluating the delegate 'getTarget'

        // (with the given object type and parameters).

        public static object GetHandle(string handleType, object[] args, Func<object[], Task<object>> getTargetAsync)

        {

            return ExcelAsyncUtil.Observe(handleType, args, () =>

            {

                var handle = $"{handleType}:{_handleIndex++}";

                var handleInfo = new HandleInfo(handle, getTargetAsync(args));

                _objects.Add(handle, handleInfo);

                return handleInfo;

            });

        }

 

        public static bool TryGetObject(string handle, out object value)

        {

            if (_objects.TryGetValue(handle, out var handleInfo))

            {

                value = handleInfo.Target;

                return true;

            }

            value = null;

            return false;

        }

 

        public static void Remove(HandleInfo handleInfo)

        {

            if (TryGetObject(handleInfo.Handle, out var value))

            {

                _objects.Remove(handleInfo.Handle);

                if (value is IDisposable disp)

                {

                    disp.Dispose();

                }

            }

        }

    }

 

    class HandleInfo : IExcelObservable, IDisposable

    {

        public readonly string Handle;

        public object Target;

 

        // Set internally when hooked up to Excel

        public IExcelObserver Observer;

 

        public HandleInfo(string handle, Task<object> getTarget)

        {

            Handle = handle;

            getTarget.ContinueWith(t =>

            {

                if (t.IsCompletedSuccessfully)

                {

                    Target = t.Result;

                    if (Observer != null)

                        Observer.OnNext(Handle);

                }

                else

                {

                    // Not sure what to do here

                    Observer.OnNext(ExcelError.ExcelErrorValue);

                    Observer.OnCompleted();

                }

            });

        }

 

        // This call is made (once) from Excel to subscribe to the topic.

        public IDisposable Subscribe(IExcelObserver observer)

        {

            // We know this will only be called once, so we take some adventurous shortcuts (like returning 'this')

            Observer = observer;

            if (Target == null)

                Observer.OnNext(ExcelError.ExcelErrorGettingData);

            else

                Observer.OnNext(Handle);

            return this;

        }

 

        public void Dispose()

        {

            HandleManager.Remove(this);               // Called when last instance of this topic is removed from the current session

        }

    }

 

 

}

AsyncFetch.gif

Hagai David

unread,
Mar 24, 2022, 12:43:45 PM3/24/22
to Excel-DNA
Thank you alot Govert its not obvious !!! =D 

ב-יום חמישי, 24 במרץ 2022 בשעה 18:07:43 UTC+2, Govert van Drimmelen כתב/ה:

Kay

unread,
Apr 13, 2022, 3:47:10 PM4/13/22
to Excel-DNA
Hello,

this question seems to circle around and i can't seem to find the solution to it.
I am trying to return an array from RTD function, but i can only get it to work for scalar values. for 2d and 3d object arrays i only every get #N/A.
From various threads
i am under the illusion, that it should be possible.

I tried extending ExcelRtdServer, implementing IRtdServer from both ExcelDna.Integration.Rtd and Microsoft.Office.Interop.Excel
I tried excel 2021, and excel 2019 via CSE (Control-Shift-Enter)
I tried returning the value from RTD() both ConnectData and RefreshData
I tried wrapper functions with or without various annotations
I tried ExcelDna 1.5.1 and 1.6.p3

All the time i get the value updates working only for scalar D1 values, while at the same time regular UDF functions that just return 0 or 1 based arrays all work fine.

Could someone please share a working snippet if ever successful.

Bellow, i will try to share one of my more bare-bones attempts. There are 3 "functions" D1 ... D3 returning array of respective dimension with string content changed every 1s.

using ExcelDna.Integration.Rtd;
//using Microsoft.Office.Interop.Excel;
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace Sample
{
    [ComVisible(true)]
    public class Rtm : IRtdServer
    {
        private IRTDUpdateEvent callback;
        private CancellationTokenSource cts;

        private string d1 = "d1";
        private string[] d2 = new string[] { "d2.1", "d2.2" };
        private string[,] d3 = new string[,] { { "d3.1", "d3.2" } , { "d3.4", "d3.4" } };
        private ConcurrentDictionary<int, string> topics = new ConcurrentDictionary<int, string>();


        public int ServerStart(IRTDUpdateEvent CallbackObject)
        {
            callback = CallbackObject;
            cts = new CancellationTokenSource();
            Task.Run(async () => {
                while (!cts.IsCancellationRequested)
                {
                    await Task.Delay(1000, cts.Token);
                    if (cts.IsCancellationRequested) return;
                    var nows = " " + DateTime.Now.ToString("O");
                    d1 = "d1.1" + nows;
                    d2 = new string[] { "d2.1" + nows, "d2.2" + nows };
                    d3 = new string[,] { { "d3.1" + nows, "d3.2" + nows }, { "d3.4" + nows, "d3.4" + nows } };
                    callback.UpdateNotify();
                }
            }).ContinueWith(t => {
                //if (t.IsFaulted) logger.LogError(t.Exception, "server exception");
                //else if (t.IsCanceled) logger.LogWarning("server task cancelled");
            });
            return 1;
        }

        public object ConnectData(int topicId, ref Array pars, ref bool newValues)
        {
            newValues = true;
            if (pars.Length == 0) return null;

            var sourceNameObj = pars.GetValue(0);
            if (!(sourceNameObj is string sourceName)) return null;

            if (sourceName == "D1" || sourceName == "D2" || sourceName == "D3")
            {
                topics[topicId] = sourceName;
                return "connect" + sourceName;
            }

            return null;
        }

        public Array RefreshData(ref int topicCount)
        {
            topicCount = topics.Count;
            object[,] result = new object[2, topicCount];
            int i = 0;
            foreach (var kv in topics)
            {
                object val = "fckp";
                switch (kv.Value)
                {
                    case "D1":
                        val = d1;
                        break;
                    case "D2":
                        val = d2;
                        break;
                    case "D3":
                        val = d3;
                        break;
                }

                result[0, i] = kv.Key;
                result[1, i] = val;
                i++;
            }
            return result;
        }

        public void DisconnectData(int topicID)
        {
            topics.TryRemove(topicID, out _);
        }

        public int Heartbeat() => 1;
        public void ServerTerminate()
        {
            cts.Cancel();
            cts.Dispose();
            callback = null;
        }
    }

    public static class Functions
    {
        //[ExcelFunction(IsVolatile = false, IsThreadSafe = true)]
        public static object Doneone()
        {
            return ExcelDna.Integration.XlCall.RTD("sample.rtm", null, "D1");
        }
        //[ExcelFunction(IsVolatile = false, IsThreadSafe = true)]
        public static object Dtwo()
        {
            return ExcelDna.Integration.XlCall.RTD("sample.rtm", null, "D2");
        }
        //[ExcelFunction(IsVolatile = false, IsThreadSafe = true)]
        public static object Dthree()
        {
            return ExcelDna.Integration.XlCall.RTD("sample.rtm", null, "D3");
        }
    }
}

Govert van Drimmelen

unread,
Apr 13, 2022, 5:19:10 PM4/13/22
to exce...@googlegroups.com

Excel RTD function cannot return an array (archive.org)

 

You can make a wrapper UDF that takes the scalar value returned from the RTD call, and based on that return an array.

One approach is to return serialized information in a string from the RTD call, the parse or unpack the string in your wrapper UDF. Note that the RTD strings are limited to 255 characters though.

Another approach is to keep some internal storage in memory, and set the RTD value to the key of your store. Then the wrapper UDF can take the key, and return the value from the store.

 

In Excel-DNA I implement the second mechanism for the async UDF and streaming IObservable support. This is built on top of an ExcelRtdServer which uses a Dictionary and Guid keys to do this. So your IExcelObservable can pass through large array values when called with ExcelAsyncUtil.Observe. In versions of Excel which support dynamic arrays, this works great.

 

In older Excel versions (2019 and earlier) which do not have Dynamic Array support, there are some memory leaks when using this pattern with CSE arrays.

So in that case the recommendation is to use an extra cell to store the key that comes from the RTD topic, and then expand the key with a different function called from different cells. Note that the example from the Microsoft KB article above where the RTD and unpacking functions are nested in a single cell is an example of unsafe usage that you should avoid.

 

-Govert

--

You received this message because you are subscribed to the Google Groups "Excel-DNA" group.
To unsubscribe from this group and stop receiving emails from it, send an email to exceldna+u...@googlegroups.com.

Reply all
Reply to author
Forward
0 new messages