C# Invoke with out parameter using .net 3.5 - c#

I want to invoke a function with two out parameters and bool as return value. Now my problem is I see that those two parameters are changed when I debug, but still they are back to length = 0 (the way they are initialized) when the function returns.
I've seen that there's a great solution for the .net-framework 4, but unfortunaly I have to use the .net-framework 3.5.
Here's my code:
public delegate bool GetAllCheckedItemsDelegate(out int[] cTypes, out int[] cFiles);
public bool GetAllCheckedItems(out int[] cTypes , out int[] cFiles )
{
if (ListView.InvokeRequired)
{
cTypes = new int[0];
cFiles = new int[0];
return (bool)ListView.Invoke(new GetAllCheckedItemsDelegate(GetAllCheckedItems), new object[] { cTypes, cFiles });
}
else
{
cTypes = new int[ListView.CheckedItems.Count];
cFiles = new int[ListView.CheckedItems.Count];
for (int i = 0; i < ListView.CheckedItems.Count; i++)
{
// ......code......
}
return (ListView.CheckedItems.Count > 0);
}
}

I do not really like "out" keyword, so what about of using a class (Row) that contains information:
using SCG = System.Collections.Generic;
using System.Linq;
public class Row {
public int CheckedType { get; set; }
public int CheckedFile { get; set; }
}
...
public delegate SCG.IEnumerable<Row> GetAllCheckedItemsDelegate();
public bool GetAllCheckedItems() {
if (ListView.InvokeRequired) {
var rows = ListView.Invoke(new GetAllCheckedItemsDelegate(GetAllCheckedItems)
, new object[] {});
return rows.Count() > 0;
} else {
var rows = new SCG.List<Row>();
for (int i = 0; i < ListView.CheckedItems.Count; i++) {
// create and set row
var row = new Row { CheckedType = x, CheckedFile = y };
...
rows.Add(row);
}
return rows.AsReadOnly();
}
}

return (bool)ListView.Invoke(..., new object[] { cTypes, cFiles });
That modifies the object[] elements. C# does not provide syntax to get it to update the arguments of the method, there's an extra level of indirection that you can't bridge with code. It is up to you to copy the references back. Nothing to worry about, you are not actually copying the array content, just the references:
var args = new object[] { null, null };
var dlg = new GetAllCheckedItemsDelegate(GetAllCheckedItems);
var retval = (bool)ListView.Invoke(dlg, args);
cTypes = (int[])args[0];
cFiles = (int[])args[1];
return retval;
Nothing pretty about it of course. Do keep in mind that you are certainly doing something very unpretty, you have no guarantee whatsoever that you get the items you expect to get. This code runs at a very unpredictable moment in time, quite disjoint from the code that's running on the UI thread. If the user is busy checking items while the worker is running then you get a quite random selection. A snapshot of the list box state that very quickly turns stale.
This almost always requires you to disable the list box while the worker is running so you provide a guarantee that the result you compute matches the list. That in turn means that it is no longer useful to write this code at all, you might as well obtain the list before you start the thread. Which is the real solution.

I've found a solution that works for me. Still I'm open to different (and better) approaches if you think that this is not a good way to solve this.
public delegate bool BoolDelegate();
public bool GetAllCheckedItems(out int[] cTypes , out int[] cFiles )
{
if (ListView.InvokeRequired)
{
int[] cTypesHelpVar = new int[0];
int[] cFilesHelpVar = new int[0];
bool ret = (bool)ListView.Invoke((BoolDelegate) (() => GetAllCheckedItems(out cTypesHelpVar, out cFilesHelpVar)));
cTypes = cTypesHelpVar;
cFiles = cFilesHelpVar;
return ret;
}
else
{
cTypes = new int[ListView.CheckedItems.Count];
cFiles = new int[ListView.CheckedItems.Count];
for (int i = 0; i < ListView.CheckedItems.Count; i++)
{
//.... code ....
}
return (ListView.CheckedItems.Count > 0);
}
}

Related

ref list isn't modified outside command class

I would like one class to modify the list fields of another class. I pass both list fields as references. The problem is, one list is modified in the end, and second is not. I don't understand why. I wanted to check my idea with a minimal working example, where everything works fine:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _20190424_ref_list_del
{
class Program
{
static void Main(string[] args)
{
ListManager testObj = new ListManager();
testObj.startJob();
}
}
class ListManager
{
private List<int> ListToExpand;
private List<int> ListToShorten;
public ListManager()
{
ListToExpand = new List<int>() { 1, 2, 3 };
ListToShorten = new List<int>() { 1, 2, 3 };
}
public void startJob()
{
EditList editObj = new EditList(ref ListToExpand, ref ListToShorten);
editObj.doJob();
System.Diagnostics.Debug.Print("List to expand:");
foreach (int x in ListToExpand)
{
System.Diagnostics.Debug.Print(x.ToString());
}
System.Diagnostics.Debug.Print("List to shorten:");
foreach (int x in ListToShorten)
{
System.Diagnostics.Debug.Print(x.ToString());
}
}
}
class EditList
{
private List<int> ListToExpand;
private List<int> ListToShorten;
public EditList(ref List<int> listToExpand, ref List<int> listToShorten)
{
ListToExpand = listToExpand;
ListToShorten = listToShorten;
}
public void doJob()
{
ListToExpand.Add(4);
ListToShorten.RemoveAt(0);
}
}
}
The main idea is:
Init two lists
Expand first and shorten another.
Check if there is an effect outside the working class, which modifies lists.
For this reason, I'm using ref. But the same approach in my real app fails. I have two lists too. One to be expanded, second to be shortened.
Let me explain what part of my code does: There is a list of samples from a Modbus device. Some of the samples require to be joined into bigger ones (because some of the values are so large that have values of int32 which need to be stored in two int16 registers).
I have a class with the Command design pattern which works as follows:
Create a Command object which takes:
(ref) input list of single samples
list of samples to be joined
(ref) list of samples joined to double ones
Build double samples
Remove single samples which have been joined
While testing, the list of double samples are filled, but the list of single ones isn't shortened. I don't understand why, because it looks like I'm using the same approach as in the minimal working example. Can you tell me what's wrong?
Test Case:
namespace put_samples_into_dbTests.Unit.Commands
{
[TestClass]
public class GetDoubleSamples
{
List<Sample> SingleSamples;
List<PatternSample32> SamplesPattern;
List<Sample32> ResultSamples;
int MaxSources = 2;
int MaxObjects = 3;
[TestInitialize]
public void TestInit()
{
SingleSamples = new List<Sample>();
SamplesPattern = new List<PatternSample32>();
ResultSamples = new List<Sample32>();
for (byte i = 1; i <= MaxSources; i++)
{
for (byte k = 2; k <= MaxObjects; k++)
{
SamplesPattern.Add(new PatternSample32(i, k, 1, 2));
}
}
}
[TestMethod]
public void BuildFrom1Set()
{
int howManySamples32 = MaxSources * (MaxObjects-1);
int howManySamples = MaxSources* ((1 + MaxObjects) * MaxObjects / 2) - howManySamples32 *2;
DateTime dateTime = DateTime.Now;
for (byte i = 1; i <= MaxSources; i++)
{
for (byte k = 1; k <= MaxObjects; k++)
{
for (byte l = 1; l <= k; l++)
{
SingleSamples.Add(new Sample(dateTime, i, k, l, 1, true));
}
}
}
Command command = new GetDoubleSamples(ref SingleSamples, SamplesPattern , ref ResultSamples );
command.Execute();
Assert.AreEqual(howManySamples, SingleSamples.Count); // [!]
Assert.AreEqual(howManySamples32, ResultSamples.Count);
}
Now, do take a look at my GetDoubleSamples command:
public class GetDoubleSamples : Command
{
private List<Sample> SingleSamples;
private List<PatternSample32> DoubleSamplesPattern;
private List<Sample32> ResultSamples;
private List<Sample> ProcessedSamples;
public GetDoubleSamples (ref List<Sample> singleSamples, List<PatternSamples32> doubleSamplesPattern, ref List<Sample32> resultSamples)
{
SingleSamples = singleSamples;
doubleSamplesPattern = doubleSamplesPattern;
ResultSamples = resultSamples;
ProcessedSamples = new List<Sample>();
}
public void Execute()
{
SortSamples();
foreach (List<Sample> samplesSet in GenerateSets())
{
ResultSamples.AddRange(BuildDoubles(samplesSet ));
}
DeleteProcessedSamples();
}
private void DeleteProcessedSamples()
{
SingleSamples = SingleSamples.Except(ProcessedSamples).ToList();
}
}
And the problem is, when I check for the amount of created double samples - it's okay. But the same (I suppose) kind of list of simple samples isn't reduced of processed samples. When I debug the code, I see that inside the class the mentioned list is modified. But once I get outside Execute methods and get outside Command class, I lose that modified list and get back to the original one. Just as if my list wasn't passed as a reference.
Do you have any idea what might be wrong?
Any suggestions, also about rearranging my test method, will be appreciated! :-)

How to write array in Excel User Defined Function

I have an Excel Addin created using Excel-DNA and have a UDF as part of the Excel Add-In. Lets say that the function is in Cell A10 and I go to delete Column Z. I have found that this action causes the function to execute again.
Is there a way to prevent this behavior from occurring? Does this have to do with the calculation model or volatility?
EDIT 1
I am using the following code to implement behavior like the Bloomberg BDH functionality - i.e. that the function is written in the first cell and the rest of the array is written via a thread.
I have 2 problems:
I cannot determine how to have a message flash in the cell where the function is written. For example, if the function is run in Cell A1, the message "Processing..." is displayed until the value of the cell has been written.
The recalculation of functions occurs with this implementation.
I have seen implementations where items 1 & 2 above are working well, but cannot determine how to do so. Does anybody have any ideas
public class ArrayWriter
{
#region Methods
#region WriteArray
[ExcelFunction(IsHidden=true)]
public object WriteArray(object[,] arrayToWrite)
{
object caller = null;
object formula = null;
AddInFacade facade;
// if not in a function
if (!ExcelDnaUtil.IsInFunctionWizard())
{
facade = new AddInFacade();
if (arrayToWrite != null)
{
// if writing more than one cell, use threads
if (arrayToWrite.GetLength(0) > 1 || arrayToWrite.GetLength(1) > 1)
{
var xlApp = ExcelDnaUtil.Application as Application;
Type xlAppType = xlApp.GetType();
caller = xlApp.Caller;
//caller = xlAppType.InvokeMember("ActiveCell", BindingFlags.GetProperty, null, xlApp, null);
formula = xlAppType.InvokeMember("FormulaR1C1Local", BindingFlags.GetProperty, null, caller, null);
// create instance of ObjectForThread and set all properties of the class
ObjectForThread threadObject = new ObjectForThread()
{
xlRef = caller,
value = arrayToWrite,
};
// create a new thread calling the method WriteFromThread and start the thread
Thread threadWriter = new Thread(() => WriteFromThread(threadObject));
threadWriter.Start();
}
else
{
facade.SetMouseCursor(XlMousePointer.xlDefault);
}
}
else
{
arrayToWrite = new object[1, 1];
arrayToWrite[0, 0] = "No data was returned.";
facade.SetMouseCursor(XlMousePointer.xlDefault);
}
}
return arrayToWrite[0,0];
}
#endregion
#region WriteFromThread
private void WriteFromThread(Object boxedThreadObject)
{
AddInFacade facade = new AddInFacade();
ObjectForThread unboxedThreadObject = (ObjectForThread)boxedThreadObject;
Object cellBelow;
Type typeCellReference = unboxedThreadObject.xlRef.GetType();
try
{
for (int i = 0; i < unboxedThreadObject.value.GetLength(0); i++)
{
for (int j = 0; j < unboxedThreadObject.value.GetLength(1); j++)
{
// do not write the first cell as this is what is returned by the function
if (i > 0 || j > 0)
{
cellBelow = typeCellReference.InvokeMember("Offset", BindingFlags.GetProperty, null, unboxedThreadObject.xlRef, new object[] { i, j });
typeCellReference.InvokeMember("Value", BindingFlags.SetProperty, null, cellBelow, new[] { Type.Missing, unboxedThreadObject.value[i, j] });
}
}
}
}
catch(Exception ex)
{
string szError = ex.Message;
}
finally
{
// attempt to kill all COM references
unboxedThreadObject.xlRef = null;
unboxedThreadObject.value = null;
//Set the mouse cursor to the default cursor since the entire array has now been written
facade.SetMouseCursor(XlMousePointer.xlDefault);
unboxedThreadObject = null;
cellBelow = null;
facade = null;
}
}
#endregion
#endregion
#region ObjectForThread Class
public class ObjectForThread
{
public object xlRef { get; set; }
public object[,] value { get; set; }
}
#endregion
}
The only way to change this behavior is to change the Application.Calculation property. But you don't want to do that. This property should only be changed momentarily, and afterward, it should be reset to the previous value. Otherwise, you're writing an addin that doesn't play well in a shared sandbox.

Adding a value to a specific location in a list of queues

Queues:
public class Queue
{
public Queue() { }
public process Front() { return this.q_list.ElementAt(0); }
public int Size { get { return this.q_list.Count; } }
public bool IsEmpty { get { return this.q_list.Count <= 0 ? true : false; } }
public void Enqueue(process proc) { this.q_list.Add(proc); }
public void Dequeue() { this.q_list.RemoveAt(0); }
public List<process> q_list = new List<process>();
};
Creation of a list:
List<Queue> rr_list = new List<Queue>();
The process struct:
public class process
{
public int Proc_a;
public int Proc_b;
public int Proc_Index;
};
Let's say I want to add a process to the list at a specific location depending on the value of Proc_Index. How can I do that? Let's also assume the list is initially empty.
process proc = new process{
Proc_a = 1,
Proc_b = 2,
Proc_Index = 4 };
I want to add that to a queue that is in the list located at index 4.
Is this possible?
I've tried:
rr_list[proc.Proc_Index].Enqueue(proc);
But it says there's an issue with index not being found or something.
The only thing I can thing of is initializing the list by adding empty queues for up to 20 indexes, but I don't know if there's a better way.
You should use a System.Collections.Generic.Queue instead of writing your own. Use a System.Collections.Generic.Dictionary if you want key-value lookup.
var rr_list = new Dictionary<int, Queue<process>>();
process proc = new process{
Proc_a = 1,
Proc_b = 2,
Proc_Index = 4 };
rr_list[proc.Proc_Index].Enqueue(proc);
You may want to use a dictionary instead of a list.
var rr_list = new Dictionary<int, Queue>();
Then have an addprocess function as such
function void AddProcess(proccess proc){
if(rr_list.ContainsKey(proc.Proc_Index){
rr_list[proc.Proc_Index].Enqueue(proc);
} else {
rr_list[proc.Proc_Index] = (new Queue()).Enqueue(proc);
}
}
A list is usually supposed to have no holes, so if you were to add an element at index 4 to an empty list, this would make indexes 0 to 3 contain null.
Now, you can do it like that. You could check if the length is bigger than the requested index, and if not, keep adding null values until it is. Then the index would exist, and you could assign something to it:
static void EnsureLength<T> (List<T> list, int index)
{
while (list.Count <= index)
list.Add(default(T));
}
Then you could use it like this:
List<int?> list = new List<int?>();
EnsureLength(list, 3);
list[3] = 123;
A possibly better way would be to simply use a Dictionary, especially if you know that you will have holes. So you would just have a Dictionary<int, T>:
Dictionary<int, int?> dict = new Dictionary<int, int?>();
dict[3] = 123;

c# Intersection and Union not working correctly

I am using C# 4.0 in VS 2010 and trying to produce either an intersection or a union of n sets of objects.
The following works correctly:
IEnumerable<String> t1 = new List<string>() { "one", "two", "three" };
IEnumerable<String> t2 = new List<string>() { "three", "four", "five" };
List<String> tInt = t1.Intersect(t2).ToList<String>();
List<String> tUnion = t1.Union(t2).ToList<String>();
// this also works
t1 = t1.Union(t2);
// as does this (but not at the same time!)
t1 = t1.Intersect(t2);
However, the following doesn't. These are code snippets.
My class is:
public class ICD10
{
public string ICD10Code { get; set; }
public string ICD10CodeSearchTitle { get; set; }
}
In the following:
IEnumerable<ICD10Codes> codes = Enumerable.Empty<ICD10Codes>();
IEnumerable<ICD10Codes> codesTemp;
List<List<String>> terms;
// I create terms here ----
// and then ...
foreach (List<string> item in terms)
{
// the following line produces the correct results
codesTemp = dataContextCommonCodes.ICD10Codes.Where(e => item.Any(k => e.ICD10CodeSearchTitle.Contains(k)));
if (codes.Count() == 0)
{
codes = codesTemp;
}
else if (intersectionRequired)
{
codes = codes.Intersect(codesTemp, new ICD10Comparer());
}
else
{
codes = codes.Union(codesTemp, new ICD10Comparer());
}
}
return codes;
The above only ever returns the results of the last item searched.
I also added my own comparer just in case, but this made no difference:
public class ICD10Comparer : IEqualityComparer<ICD10Codes>
{
public bool Equals(ICD10Codes Code1, ICD10Codes Code2)
{
if (Code1.ICD10Code == Code2.ICD10Code) { return true; }
return false;
}
public int GetHashCode(ICD10Codes Code1)
{
return Code1.ICD10Code.GetHashCode();
}
}
I am certain I am overlooking something obvious - I just cannot see what it is!
This code: return codes; returns a deferred enumerable. None of the queries have been executed to fill the set. Some queries get executed each time through the loop to make a Count though.
This deferred execution is a problem because of the closure issue... at the return, item is bound to the last loop execution.
Resolve this by forcing the queries to execute in each loop execution:
if (codes.Count() == 0)
{
codes = codesTemp.ToList();
}
else if (intersectionRequired)
{
codes = codes.Intersect(codesTemp, new ICD10Comparer()).ToList();
}
else
{
codes = codes.Union(codesTemp, new ICD10Comparer()).ToList();
}
if you are using an own comparer, you should take a look at the correct implementation of the GetHashCode function. the linq operators use this comparison too. you can take a look here:
http://msdn.microsoft.com/en-us/library/system.object.gethashcode(v=vs.80).aspx
you could try changing the hash function to "return 0", to see if it is the problem. ICD10Code.GetHashCode will return perhaps different values if it is a class object
Your problem definitely is not connect to Intersect or Union LINQ extension methods. I've just tested following:
var t1 = new List<ICD10>()
{
new ICD10() { ICD10Code = "123" },
new ICD10() { ICD10Code = "234" },
new ICD10() { ICD10Code = "345" }
};
var t2 = new List<ICD10>()
{
new ICD10() { ICD10Code = "234" },
new ICD10() { ICD10Code = "456" }
};
// returns list with just one element - the one with ICF10Code == "234"
var results = t1.Intersect(t2, new ICD10Comparer()).ToList();
// return list with 4 elements
var results2 = t1.Union(t2, new ICD10Comparer()).ToList();
Using your ICD10 and ICD10Comparer classes declarations. Everything works just fine! You have to search for bug in your custom code, because LINQ works just fine.

Excel ExcelDNA C# / Try to copy Bloomberg BDH() behavior (writing Array after a web request)

I want to copy Bloomberg BDH behavior.
BDH makes a web request and write an array (but doesn't return an array style). During this web request, the function returns "#N/A Requesting".
When the web request finished, the BDH() function writes the array result in the worksheet.
For example, in ExcelDNA, I succeed to write in the worksheet with a thread.
The result if you use the code below in a DNA file, the result of
=WriteArray(2;2)
will be
Line 1 > #N/A Requesting Data (0,1)
Line 2 > (1,0) (1,1)
The last issue is to replace #N/A Requesting Data with the value and copy the formula.
When you uncomment //xlActiveCellType.InvokeMember("FormulaR1C1Local", you are near the result but you don't have the right behavior
File .dna
<DnaLibrary Language="CS" RuntimeVersion="v4.0">
<![CDATA[
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using ExcelDna.Integration;
public static class WriteForXL
{
public static object[,] MakeArray(int rows, int columns)
{
if (rows == 0 && columns == 0)
{
rows = 1;
columns = 1;
}
object[,] result = new string[rows, columns];
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < columns; j++)
{
result[i, j] = string.Format("({0},{1})", i, j);
}
}
return result;
}
public static object WriteArray(int rows, int columns)
{
if (ExcelDnaUtil.IsInFunctionWizard())
return "Waiting for click on wizard ok button to calculate.";
object[,] result = MakeArray(rows, columns);
var xlApp = ExcelDnaUtil.Application;
Type xlAppType = xlApp.GetType();
object caller = xlAppType.InvokeMember("ActiveCell", BindingFlags.GetProperty, null, xlApp, null);
object formula = xlAppType.InvokeMember("FormulaR1C1Local", BindingFlags.GetProperty, null, caller, null);
ObjectForThread q = new ObjectForThread() { xlRef = caller, value = result, FormulaR1C1Local = formula };
Thread t = new Thread(WriteFromThread);
t.Start(q);
return "#N/A Requesting Data";
}
private static void WriteFromThread(Object o)
{
ObjectForThread q = (ObjectForThread) o;
Type xlActiveCellType = q.xlRef.GetType();
try
{
for (int i = 0; i < q.value.GetLength(0); i++)
{
for (int j = 0; j < q.value.GetLength(1); j++)
{
if (i == 0 && j == 0)
continue;
Object cellBelow = xlActiveCellType.InvokeMember("Offset", BindingFlags.GetProperty, null, q.xlRef, new object[] { i, j });
xlActiveCellType.InvokeMember("Value", BindingFlags.SetProperty, null, cellBelow, new[] { Type.Missing, q.value[i, j] });
}
}
}
catch(Exception e)
{
}
finally
{
//xlActiveCellType.InvokeMember("Value", BindingFlags.SetProperty, null, q.xlRef, new[] { Type.Missing, q.value[0, 0] });
//xlActiveCellType.InvokeMember("FormulaR1C1Local", BindingFlags.SetProperty, null, q.xlRef, new [] { q.FormulaR1C1Local });
}
}
public class ObjectForThread
{
public object xlRef { get; set; }
public object[,] value { get; set; }
public object FormulaR1C1Local { get; set; }
}
}
]]>
</DnaLibrary>
#To Govert
BDH has become a standard in finance industry. People do not know how to manipulate an array (even the Ctrl+Shift+Enter).
BDH is the function that made Bloomberg so popular (to the disadvantage of Reuters).
However I will think of using your method or RTD.
Thanks for all your work in Excel DNA
I presume you have tried the Excel-DNA ArrayResizer sample, which carefully avoids many of the issue you are running into. I'd like to understand what you see as the disadvantages of the array-formula-writing approach.
Now, about your function:
Firstly, you can't safely pass the 'caller' Range COM object to another thread - rather pass a string with the address, and get the COM object from the other thread (using a call to ExcelDnaUtil.Application on the worker thread). Most of the time you'll get lucky, though.
The better way to do this is from the worker thread to get Excel to run a macro on the main thread - by calling Application.Run. The Excel-DNA ArrayResizer sample shows how this can be done.
Secondly, you almost certainly don't want the ActiveCell, but rather Application.Caller. The ActiveCell might well have nothing to do with the cell where the formula is running from.
Next - Excel will recalculate your function every time you set the Formula again - hence putting you in an endless loop when you enable the Formula set in your finally clause. You cannot set both the Value and the Formula for a cell - if a cell has a Formula then Excel will use the formula to calculate the Value. If you set the Value, the Formula gets removed.
It's not clear what you want to actually leave in the [0,0] cell - IIRC Bloomberg modifies the formula there in a way that makes it remember how large a range was written to. You could try to add some parameters to your function that tell your function whether to recalculate or whether to return an actual value as its result.
Finally, you might want to reconsider whether the Bloomberg BDH function is a good example for what you want to do. It breaks the dependency calculation of your sheet, which has implications both for performance and for maintaining consistency of the spreadsheet model.
My issue was :
writing dynamic array
data are retrieved asynchronous via a webservice
After discussing with Govert, I chose to take a result as an array and not to copy Bloomberg functions (write an array but return a single value).
Finally, to solve my issue, I used http://excel-dna.net/2011/01/30/resizing-excel-udf-result-arrays/
and reshape the resize() function.
This code is not RTD.
The code belows works in a .dna file
<DnaLibrary RuntimeVersion="v4.0" Language="C#">
<![CDATA[
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.ComponentModel;
using ExcelDna.Integration;
public static class ResizeTest
{
public static object[,] MakeArray(int rows, int columns)
{
object[,] result = new string[rows, columns];
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < columns; j++)
{
result[i,j] = string.Format("({0},{1})", i, j);
}
}
return result;
}
public static object MakeArrayAndResize()
{
// Call Resize via Excel - so if the Resize add-in is not part of this code, it should still work.
return XlCall.Excel(XlCall.xlUDF, "Resize", null);
}
}
public class Resizer
{
static Queue<ExcelReference> ResizeJobs = new Queue<ExcelReference>();
static Dictionary<string, object> JobIsDone = new Dictionary<string, object>();
// This function will run in the UDF context.
// Needs extra protection to allow multithreaded use.
public static object Resize(object args)
{
ExcelReference caller = XlCall.Excel(XlCall.xlfCaller) as ExcelReference;
if (caller == null)
return ExcelError.ExcelErrorNA;
if (!JobIsDone.ContainsKey(GetHashcode(caller)))
{
BackgroundWorker(caller);
return ExcelError.ExcelErrorNA;
}
else
{
// Size is already OK - just return result
object[,] array = (object[,])JobIsDone[GetHashcode(caller)];
JobIsDone.Remove(GetHashcode(caller));
return array;
}
}
/// <summary>
/// Simulate WebServiceRequest
/// </summary>
/// <param name="caller"></param>
/// <param name="rows"></param>
/// <param name="columns"></param>
static void BackgroundWorker(ExcelReference caller)
{
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += (sender, args) =>
{
Thread.Sleep(3000);
};
bw.RunWorkerCompleted += (sender, args) =>
{
// La requete
Random r = new Random();
object[,] array = ResizeTest.MakeArray(r.Next(10), r.Next(10));
JobIsDone[GetHashcode(caller)] = array;
int rows = array.GetLength(0);
int columns = array.GetLength(1);
EnqueueResize(caller, rows, columns);
AsyncRunMacro("DoResizing");
};
bw.RunWorkerAsync();
}
static string GetHashcode(ExcelReference caller)
{
return caller.SheetId + ":L" + caller.RowFirst + "C" + caller.ColumnFirst;
}
static void EnqueueResize(ExcelReference caller, int rows, int columns)
{
ExcelReference target = new ExcelReference(caller.RowFirst, caller.RowFirst + rows - 1, caller.ColumnFirst, caller.ColumnFirst + columns - 1, caller.SheetId);
ResizeJobs.Enqueue(target);
}
public static void DoResizing()
{
while (ResizeJobs.Count > 0)
{
DoResize(ResizeJobs.Dequeue());
}
}
static void DoResize(ExcelReference target)
{
try
{
// Get the current state for reset later
XlCall.Excel(XlCall.xlcEcho, false);
// Get the formula in the first cell of the target
string formula = (string)XlCall.Excel(XlCall.xlfGetCell, 41, target);
ExcelReference firstCell = new ExcelReference(target.RowFirst, target.RowFirst, target.ColumnFirst, target.ColumnFirst, target.SheetId);
bool isFormulaArray = (bool)XlCall.Excel(XlCall.xlfGetCell, 49, target);
if (isFormulaArray)
{
object oldSelectionOnActiveSheet = XlCall.Excel(XlCall.xlfSelection);
object oldActiveCell = XlCall.Excel(XlCall.xlfActiveCell);
// Remember old selection and select the first cell of the target
string firstCellSheet = (string)XlCall.Excel(XlCall.xlSheetNm, firstCell);
XlCall.Excel(XlCall.xlcWorkbookSelect, new object[] {firstCellSheet});
object oldSelectionOnArraySheet = XlCall.Excel(XlCall.xlfSelection);
XlCall.Excel(XlCall.xlcFormulaGoto, firstCell);
// Extend the selection to the whole array and clear
XlCall.Excel(XlCall.xlcSelectSpecial, 6);
ExcelReference oldArray = (ExcelReference)XlCall.Excel(XlCall.xlfSelection);
oldArray.SetValue(ExcelEmpty.Value);
XlCall.Excel(XlCall.xlcSelect, oldSelectionOnArraySheet);
XlCall.Excel(XlCall.xlcFormulaGoto, oldSelectionOnActiveSheet);
}
// Get the formula and convert to R1C1 mode
bool isR1C1Mode = (bool)XlCall.Excel(XlCall.xlfGetWorkspace, 4);
string formulaR1C1 = formula;
if (!isR1C1Mode)
{
// Set the formula into the whole target
formulaR1C1 = (string)XlCall.Excel(XlCall.xlfFormulaConvert, formula, true, false, ExcelMissing.Value, firstCell);
}
// Must be R1C1-style references
object ignoredResult;
XlCall.XlReturn retval = XlCall.TryExcel(XlCall.xlcFormulaArray, out ignoredResult, formulaR1C1, target);
if (retval != XlCall.XlReturn.XlReturnSuccess)
{
// TODO: Consider what to do now!?
// Might have failed due to array in the way.
firstCell.SetValue("'" + formula);
}
}
finally
{
XlCall.Excel(XlCall.xlcEcho, true);
}
}
// Most of this from the newsgroup: http://groups.google.com/group/exceldna/browse_thread/thread/a72c9b9f49523fc9/4577cd6840c7f195
private static readonly TimeSpan BackoffTime = TimeSpan.FromSeconds(1);
static void AsyncRunMacro(string macroName)
{
// Do this on a new thread....
Thread newThread = new Thread( delegate ()
{
while(true)
{
try
{
RunMacro(macroName);
break;
}
catch(COMException cex)
{
if(IsRetry(cex))
{
Thread.Sleep(BackoffTime);
continue;
}
// TODO: Handle unexpected error
return;
}
catch(Exception ex)
{
// TODO: Handle unexpected error
return;
}
}
});
newThread.Start();
}
static void RunMacro(string macroName)
{
object xlApp = null;
try
{
xlApp = ExcelDnaUtil.Application;
xlApp.GetType().InvokeMember("Run", BindingFlags.InvokeMethod, null, xlApp, new object[] {macroName});
}
catch (TargetInvocationException tie)
{
throw tie.InnerException;
}
finally
{
Marshal.ReleaseComObject(xlApp);
}
}
const uint RPC_E_SERVERCALL_RETRYLATER = 0x8001010A;
const uint VBA_E_IGNORE = 0x800AC472;
static bool IsRetry(COMException e)
{
uint errorCode = (uint)e.ErrorCode;
switch(errorCode)
{
case RPC_E_SERVERCALL_RETRYLATER:
case VBA_E_IGNORE:
return true;
default:
return false;
}
}
}
]]>
</DnaLibrary>
I think you need to implemented the request as a RTD server. Normal user defined functions will not update asynchronously.
Then you may hide the call of the RTD server via a user defined function, which can be done via Excel-DNA.
So finally you use Array formula, right? As you said, users are not familiar with array formula, they do not know ctrl+shift+enter. I think array formula is a big problem for them.
For me, I have the same issue. I am trying to build a prototype for it. see
https://github.com/kchen0723/ExcelAsync.git

Categories