I think the answer is No, there isn't within VS Crystal Reports. It looks like there's an API for other versions, e.g. this
As an alternative, I've changed to having lots of code in my report formula instead of using custom functions. I can then update the report formula using ReportDocument.DataDefinition.FormulaFields..Text
In my case I only want to update one formula in each report, named 'Period'. I've created a file PeriodFormula.txt and included it in the project with Build Action = EmbeddedResource.
I created this class to read the txt file and update all reports within a given directory. It's currently hardcoded to only update the Period formula, but could easily be modified to operate from a list etc.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using CrystalDecisions.CrystalReports.Engine;
using CrystalDecisions.Shared;
namespace RMReports
{
public class CustomFunctionUpdater
{
/// <summary>
/// Update all rpt files in the given directory and all subdirectories.
/// Currently only updates the Period formula.
/// </summary>
/// <param name="directoryPath"></param>
public static void UpdateAllReports(String directoryPath)
{
Debug.WriteLine(string.Format("Starting update on all reports within {0}", directoryPath));
const string formulaName = "Period";
int reportsUpdated = 0;
string formulaText = GetFormulaText(formulaName);
foreach (String filename in Directory.GetFiles(directoryPath, "*.rpt", SearchOption.AllDirectories))
{
try
{
if (UpdateReportFunction(filename, formulaName, formulaText))
{
reportsUpdated++;
Debug.WriteLine(string.Format("Updated: {0}", filename));
}
else
Debug.WriteLine(string.Format("No update to: {0}", filename));
}
catch(Exception ex)
{
Debug.WriteLine(string.Format("Failed to update: {0}. Error: {1}", filename, ex.Message));
}
}
Debug.WriteLine(string.Format("done. {0} reports updated", reportsUpdated));
}
/// <summary>
/// Opens the given report file, updates the specified formula with the given text
/// and saves the report.
/// </summary>
/// <param name="reportFilename">The report file to update</param>
/// <param name="formulaName">The name of the formula to update</param>
/// <param name="formulaText">The new text of the formula to update</param>
/// <returns>Whether the report was updated. If the formula doesn't exist this will be false.</returns>
public static bool UpdateReportFunction(String reportFilename, String formulaName, string formulaText)
{
if (String.IsNullOrEmpty(formulaText)) return false;
if (!File.Exists(reportFilename)) throw new FileNotFoundException("reportFilename", reportFilename);
bool updated = false;
ReportDocument document = new ReportDocument();
try
{
document.Load(reportFilename, OpenReportMethod.OpenReportByDefault);
foreach (FormulaFieldDefinition f in document.DataDefinition.FormulaFields)
{
if (f.Name != formulaName) continue;
if (f.Text == formulaText) break; // no update needed
f.Text = formulaText;
updated = true;
break;
}
if (updated)
document.SaveAs(reportFilename);
}
finally
{
if (document.IsLoaded)
document.Close();
}
return updated;
}
public static void UpdateReportFunction(String reportFilename, String formulaName)
{
string formulaText = GetFormulaText(formulaName);
UpdateReportFunction(reportFilename, formulaName, formulaText);
}
/// <summary>
/// Reads the text for the given formula from the current assembly. Assumes the formula
/// exists in a file named [formulaName]Formula.txt that's been compiled as an embedded resource
/// in the current assembly, e.g. DoStuffFormula.txt for a formula named DoStuff.
/// </summary>
/// <param name="formulaName"></param>
/// <returns></returns>
public static String GetFormulaText(String formulaName)
{
string resourceName = Assembly.GetExecutingAssembly().GetName().Name + "." + formulaName + "Formula.txt";
Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
if (stream==null) return null;
return (new StreamReader(stream)).ReadToEnd();
}
}
}
Then I use it like this, to update all my reports (which are in folders beneath a 'reports' folder).
DirectoryInfo d = Directory.GetParent(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
string reportDirectory = Path.Combine(d.Parent.FullName, "reports");
CustomFunctionUpdater.UpdateAllReports(reportDirectory);
Hopefully someone else finds this useful!