blob: 082da63317835cafe13a6adcebb13a71ef15fe14 [file] [log] [blame]
/*******************************************************************************
* You may amend and distribute as you like, but don't remove this header!
*
* EPPlus provides server-side generation of Excel 2007/2010 spreadsheets.
* See http://www.codeplex.com/EPPlus for details.
*
* Copyright (C) 2011 Jan Källman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php
* If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html
*
* All code and executables are provided "as is" with no warranty either express or implied.
* The author accepts no liability for any damage or loss of business that this product may cause.
*
* Code change notes:
*
* Author Change Date
* ******************************************************************************
* Eyal Seagull Added 2012-04-03
*******************************************************************************/
using System;
using System.Collections.Immutable;
using System.Xml;
using OfficeOpenXml.ConditionalFormatting.Contracts;
using OfficeOpenXml.Style.Dxf;
using OfficeOpenXml.Utils;
namespace OfficeOpenXml.ConditionalFormatting;
/// <summary>
///
/// </summary>
public abstract class ExcelConditionalFormattingRule : XmlHelper, IExcelConditionalFormattingRule {
private eExcelConditionalFormattingRuleType? _type;
private readonly ExcelWorksheet _worksheet;
/// <summary>
/// Sinalize that we are in a Cnaging Priorities opeartion so that we won't enter
/// a recursive loop.
/// </summary>
private static bool _changingPriority;
protected override ImmutableArray<string> SchemaNodeOrder =>
ExcelWorksheet.WorksheetSchemaNodeOrder;
/// <summary>
/// Initialize the <see cref="ExcelConditionalFormattingRule"/>
/// </summary>
/// <param name="type"></param>
/// <param name="address"></param>
/// <param name="priority">Used also as the cfRule unique key</param>
/// <param name="worksheet"></param>
/// <param name="itemElementNode"></param>
/// <param name="namespaceManager"></param>
internal ExcelConditionalFormattingRule(
eExcelConditionalFormattingRuleType type,
ExcelAddress address,
int priority,
ExcelWorksheet worksheet,
XmlNode itemElementNode,
XmlNamespaceManager namespaceManager)
: base(namespaceManager, itemElementNode) {
Require.Argument(address).IsNotNull("address");
Require.Argument(priority).IsInRange(1, int.MaxValue, "priority");
Require.Argument(worksheet).IsNotNull("worksheet");
_type = type;
_worksheet = worksheet;
if (itemElementNode == null) {
// Create/Get the <cfRule> inside <conditionalFormatting>
itemElementNode = CreateComplexNode(
_worksheet.WorksheetXml.DocumentElement,
string.Format(
"{0}[{1}='{2}']/{1}='{2}'/{3}[{4}='{5}']/{4}='{5}'",
//{0}
ExcelConditionalFormattingConstants.Paths._conditionalFormatting,
// {1}
ExcelConditionalFormattingConstants.Paths._sqrefAttribute,
// {2}
address.AddressSpaceSeparated, //CF node don't what to have comma between multi addresses, use space instead.
// {3}
ExcelConditionalFormattingConstants.Paths._cfRule,
//{4}
ExcelConditionalFormattingConstants.Paths._priorityAttribute,
//{5}
priority));
}
// Point to <cfRule>
TopNode = itemElementNode;
Address = address;
Priority = priority;
Type = type;
if (DxfId >= 0) {
worksheet.Workbook.Styles.Dxfs[DxfId].AllowChange = true; //This Id is referenced by CF, so we can use it when we save.
_style = worksheet.Workbook.Styles.Dxfs[DxfId].Clone(); //Clone, so it can be altered without effecting other dxf styles
}
}
/// <summary>
/// Initialize the <see cref="ExcelConditionalFormattingRule"/>
/// </summary>
/// <param name="type"></param>
/// <param name="address"></param>
/// <param name="priority"></param>
/// <param name="worksheet"></param>
/// <param name="namespaceManager"></param>
internal ExcelConditionalFormattingRule(
eExcelConditionalFormattingRuleType type,
ExcelAddress address,
int priority,
ExcelWorksheet worksheet,
XmlNamespaceManager namespaceManager)
: this(type, address, priority, worksheet, null, namespaceManager) {}
/// <summary>
/// Get the &lt;cfRule&gt; node
/// </summary>
public XmlNode Node => TopNode;
/// <summary>
/// Address of the conditional formatting rule
/// </summary>
/// <remarks>
/// The address is stores in a parent node called &lt;conditionalFormatting&gt; in the
/// @sqref attribute. Excel groups rules that have the same address inside one node.
/// </remarks>
public ExcelAddress Address {
get =>
new(Node.ParentNode.Attributes[ExcelConditionalFormattingConstants.Attributes._sqref].Value);
set {
// Check if the address is to be changed
if (Address.Address != value.Address) {
// Save the old parente node
XmlNode oldNode = Node;
XmlNode oldParentNode = Node.ParentNode;
// Create/Get the new <conditionalFormatting> parent node
XmlNode newParentNode = CreateComplexNode(
_worksheet.WorksheetXml.DocumentElement,
string.Format(
"{0}[{1}='{2}']/{1}='{2}'",
//{0}
ExcelConditionalFormattingConstants.Paths._conditionalFormatting,
// {1}
ExcelConditionalFormattingConstants.Paths._sqrefAttribute,
// {2}
value.AddressSpaceSeparated));
// Move the <cfRule> node to the new <conditionalFormatting> parent node
TopNode = newParentNode.AppendChild(Node);
// Check if the old <conditionalFormatting> parent node has <cfRule> node inside it
if (!oldParentNode.HasChildNodes) {
// Remove the old parent node
oldParentNode.ParentNode.RemoveChild(oldParentNode);
}
}
}
}
/// <summary>
/// Type of conditional formatting rule. ST_CfType §18.18.12.
/// </summary>
public eExcelConditionalFormattingRuleType Type {
get {
// Transform the @type attribute to EPPlus Rule Type (slighty diferente)
if (_type == null) {
_type = ExcelConditionalFormattingRuleType.GetTypeByAttrbiute(
GetXmlNodeString(ExcelConditionalFormattingConstants.Paths._typeAttribute),
TopNode,
_worksheet.NameSpaceManager);
}
return (eExcelConditionalFormattingRuleType)_type;
}
internal set {
_type = value;
// Transform the EPPlus Rule Type to @type attribute (slighty diferente)
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._typeAttribute,
ExcelConditionalFormattingRuleType.GetAttributeByType(value),
true);
}
}
/// <summary>
/// The priority of this conditional formatting rule. This value is used to determine
/// which format should be evaluated and rendered. Lower numeric values are higher
/// priority than higher numeric values, where 1 is the highest priority.
/// </summary>
public int Priority {
get => GetXmlNodeInt(ExcelConditionalFormattingConstants.Paths._priorityAttribute);
set {
// Save the current CF rule priority
int priority = Priority;
// Check if the @priority is to be changed
if (priority != value) {
// Check if we are not already inside a "Change Priority" operation
if (!_changingPriority) {
if (value < 1) {
throw new IndexOutOfRangeException(
ExcelConditionalFormattingConstants.Errors._invalidPriority);
}
// Sinalize that we are already changing cfRules priorities
_changingPriority = true;
// Check if we lowered the priority
if (priority > value) {
for (int i = priority - 1; i >= value; i--) {
var cfRule = _worksheet.ConditionalFormatting.RulesByPriority(i);
if (cfRule != null) {
cfRule.Priority++;
}
}
} else {
for (int i = priority + 1; i <= value; i++) {
var cfRule = _worksheet.ConditionalFormatting.RulesByPriority(i);
if (cfRule != null) {
cfRule.Priority--;
}
}
}
// Sinalize that we are no longer changing cfRules priorities
_changingPriority = false;
}
// Change the priority in the XML
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._priorityAttribute,
value.ToString(),
true);
}
}
}
/// <summary>
/// If this flag is true, no rules with lower priority shall be applied over this rule,
/// when this rule evaluates to true.
/// </summary>
public bool StopIfTrue {
get => GetXmlNodeBool(ExcelConditionalFormattingConstants.Paths._stopIfTrueAttribute);
set =>
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._stopIfTrueAttribute,
value ? "1" : string.Empty,
true);
}
/// <summary>
/// DxfId Style Attribute
/// </summary>
internal int DxfId {
get => GetXmlNodeInt(ExcelConditionalFormattingConstants.Paths._dxfIdAttribute);
set =>
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._dxfIdAttribute,
(value == int.MinValue) ? string.Empty : value.ToString(),
true);
}
internal ExcelDxfStyleConditionalFormatting _style;
public ExcelDxfStyleConditionalFormatting Style {
get {
if (_style == null) {
_style = new(NameSpaceManager, null, _worksheet.Workbook.Styles);
}
return _style;
}
}
/// <summary>
/// StdDev (zero is not allowed and will be converted to 1)
/// </summary>
public UInt16 StdDev {
get =>
Convert.ToUInt16(GetXmlNodeInt(ExcelConditionalFormattingConstants.Paths._stdDevAttribute));
set =>
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._stdDevAttribute,
(value == 0) ? "1" : value.ToString(),
true);
}
/// <summary>
/// Rank (zero is not allowed and will be converted to 1)
/// </summary>
public UInt16 Rank {
get =>
Convert.ToUInt16(GetXmlNodeInt(ExcelConditionalFormattingConstants.Paths._rankAttribute));
set =>
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._rankAttribute,
(value == 0) ? "1" : value.ToString(),
true);
}
/// <summary>
/// AboveAverage
/// </summary>
internal protected bool? AboveAverage {
get {
bool? aboveAverage = GetXmlNodeBoolNullable(
ExcelConditionalFormattingConstants.Paths._aboveAverageAttribute);
// Above Avarege if TRUE or if attribute does not exists
return (aboveAverage == true) || (aboveAverage == null);
}
set {
string aboveAverageValue = string.Empty;
// Only the types that needs the @AboveAverage
if ((_type == eExcelConditionalFormattingRuleType.BelowAverage)
|| (_type == eExcelConditionalFormattingRuleType.BelowOrEqualAverage)
|| (_type == eExcelConditionalFormattingRuleType.BelowStdDev)) {
aboveAverageValue = "0";
}
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._aboveAverageAttribute,
aboveAverageValue,
true);
}
}
/// <summary>
/// EqualAverage
/// </summary>
internal protected bool? EqualAverage {
get {
bool? equalAverage = GetXmlNodeBoolNullable(
ExcelConditionalFormattingConstants.Paths._equalAverageAttribute);
// Equal Avarege only if TRUE
return (equalAverage == true);
}
set {
string equalAverageValue = string.Empty;
// Only the types that needs the @EqualAverage
if ((_type == eExcelConditionalFormattingRuleType.AboveOrEqualAverage)
|| (_type == eExcelConditionalFormattingRuleType.BelowOrEqualAverage)) {
equalAverageValue = "1";
}
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._equalAverageAttribute,
equalAverageValue,
true);
}
}
/// <summary>
/// Bottom attribute
/// </summary>
internal protected bool? Bottom {
get {
bool? bottom = GetXmlNodeBoolNullable(
ExcelConditionalFormattingConstants.Paths._bottomAttribute);
// Bottom if TRUE
return (bottom == true);
}
set {
string bottomValue = string.Empty;
// Only the types that needs the @Bottom
if ((_type == eExcelConditionalFormattingRuleType.Bottom)
|| (_type == eExcelConditionalFormattingRuleType.BottomPercent)) {
bottomValue = "1";
}
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._bottomAttribute,
bottomValue,
true);
}
}
/// <summary>
/// Percent attribute
/// </summary>
internal protected bool? Percent {
get {
bool? percent = GetXmlNodeBoolNullable(
ExcelConditionalFormattingConstants.Paths._percentAttribute);
// Bottom if TRUE
return (percent == true);
}
set {
string percentValue = string.Empty;
// Only the types that needs the @Bottom
if ((_type == eExcelConditionalFormattingRuleType.BottomPercent)
|| (_type == eExcelConditionalFormattingRuleType.TopPercent)) {
percentValue = "1";
}
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._percentAttribute,
percentValue,
true);
}
}
/// <summary>
/// TimePeriod
/// </summary>
internal protected eExcelConditionalFormattingTimePeriodType TimePeriod {
get =>
ExcelConditionalFormattingTimePeriodType.GetTypeByAttribute(
GetXmlNodeString(ExcelConditionalFormattingConstants.Paths._timePeriodAttribute));
set =>
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._timePeriodAttribute,
ExcelConditionalFormattingTimePeriodType.GetAttributeByType(value),
true);
}
/// <summary>
/// Operator
/// </summary>
internal protected eExcelConditionalFormattingOperatorType Operator {
get =>
ExcelConditionalFormattingOperatorType.GetTypeByAttribute(
GetXmlNodeString(ExcelConditionalFormattingConstants.Paths._operatorAttribute));
set =>
SetXmlNodeString(
ExcelConditionalFormattingConstants.Paths._operatorAttribute,
ExcelConditionalFormattingOperatorType.GetAttributeByType(value),
true);
}
/// <summary>
/// Formula
/// </summary>
public string Formula {
get => GetXmlNodeString(ExcelConditionalFormattingConstants.Paths._formula);
set => SetXmlNodeString(ExcelConditionalFormattingConstants.Paths._formula, value);
}
/// <summary>
/// Formula2
/// </summary>
public string Formula2 {
get =>
GetXmlNodeString(
string.Format(
"{0}[position()=2]",
// {0}
ExcelConditionalFormattingConstants.Paths._formula));
set {
// Create/Get the first <formula> node (ensure that it exists)
var firstNode = CreateComplexNode(
TopNode,
string.Format(
"{0}[position()=1]",
// {0}
ExcelConditionalFormattingConstants.Paths._formula));
// Create/Get the seconde <formula> node (ensure that it exists)
var secondNode = CreateComplexNode(
TopNode,
string.Format(
"{0}[position()=2]",
// {0}
ExcelConditionalFormattingConstants.Paths._formula));
// Save the formula in the second <formula> node
secondNode.InnerText = value;
}
}
}