Zum Inhalt springen
← Alle Artikel

Download-Button mit einer SPFx-Erweiterung erstellen

In diesem Beitrag erfährst du, wie du die Befehlsleiste einer SharePoint-Liste mit einer SPFx-Erweiterung anpasst und den Download-Button um eigene Logik erweiterst.

Vor einiger Zeit habe ich in einem Projekt an einem Dokumentenmanagement-System in SharePoint Online gearbeitet. In diesem Setup dürfen auch Gastbenutzer Dateien aus der Dokumentbibliothek lesen, bearbeiten und herunterladen.

Dabei gab es eine ganz konkrete Anforderung: Es sollte nachvollziehbar sein, welcher Gastbenutzer welche Datei herunterlädt. Und zwar so, dass eine zuständige Person automatisch eine E-Mail bekommt, sobald jemand eine Datei aus der Bibliothek lädt.

Das Problem dabei: Der „Herunterladen”-Button in der SharePoint-Befehlsleiste bringt diese Funktion einfach nicht mit. Es gibt keinen Schalter, mit dem man „bei Download eine E-Mail senden” aktiviert. Nach etwas Recherche haben wir aber einen gangbaren Weg gefunden, nämlich eine SPFx List View Command Set Extension, mit der sich der vorhandene Button erweitern lässt, ohne ihn zu ersetzen.

In diesem Artikel gehe ich Schritt für Schritt durch, wie du den Download-Button in der SharePoint-Befehlsleiste so anpasst, dass beim Herunterladen einer Datei automatisch eine E-Mail rausgeht. Komplett mit dem SharePoint Framework (SPFx).

Hinweis vorab: Das hier gezeigte Vorgehen hängt sich an einen internen SharePoint-Button. Das funktioniert gut, ist aber kein offiziell von Microsoft unterstützter Weg. Was das praktisch bedeutet, steht weiter unten im Abschnitt „Grenzen dieses Ansatzes”. Lies den, bevor du das Ganze produktiv einsetzt.

Worum es geht: den vorhandenen Button erweitern statt ersetzen

Mit einer List View Command Set Extension kann man im SharePoint Framework die Befehlsleiste und das Kontextmenü von Listen und Bibliotheken anpassen. Normalerweise nutzt man das, um eigene Buttons hinzuzufügen.

In unserem Fall ist die Aufgabe aber eine andere: Wir wollen keinen neuen Button bauen, sondern den vorhandenen „Herunterladen”-Button in seiner Funktion erweitern. Konkret soll beim Klick zusätzlich eine E-Mail an eine zuständige Person verschickt werden, die darüber informiert, wer welche Datei geladen hat.

Der Trick dabei: Wir hängen uns mit einem eigenen Event-Handler an den Standard-Button, anstatt ihn nachzubauen. Dafür brauchen wir aber zuerst eine eindeutige Kennung dieses Buttons.

Die Kennung des Download-Buttons finden

Damit wir uns an den richtigen Button hängen können, müssen wir wissen, wie SharePoint ihn intern markiert. Diese Kennung finden wir über die Entwicklertools des Browsers. So gehst du vor:

  1. Öffne die SharePoint-Dokumentbibliothek, markiere ein oder zwei Dateien. Dann erscheint in der Befehlsleiste (oder im Kontextmenü) der Button „Herunterladen”.
  2. Drücke jetzt gleichzeitig Strg + Umschalt + I. Alternativ machst du einen Rechtsklick und wählst im Menü Untersuchen aus.

Die Entwicklertools öffnen sich direkt über der SharePoint-Befehlsleiste.

  1. Es öffnet sich das Inspektor-Fenster. Klicke oben in der Symbolleiste links auf das erste Icon (den Element-Auswähler). Fahre dann mit dem Mauszeiger über den „Herunterladen”-Button und klicke ihn an. Auf der rechten Seite siehst du nun den dazugehörigen HTML-Code. Wichtig ist hier das Attribut data-automationid mit dem Wert downloadCommand. Genau über dieses Attribut sprechen wir den Button später im Code an.

Das Attribut data-automationid mit dem Wert downloadCommand identifiziert den Standard-Button.

Mit dieser Kennung können wir den Button später im Code zuverlässig finden. Jetzt geht es an die eigentliche Erweiterung.

Die SPFx List View Command Set Extension erstellen

Jetzt bauen wir die Erweiterung. Sie nutzt Microsoft Graph, um E-Mails zu versenden. Damit lassen sich übrigens sowohl interne als auch externe Nutzer benachrichtigen.

1. Öffne die Eingabeaufforderung und lege mit den folgenden Befehlen den Projektordner an:

md SPFxListViewExtDownload

cd SPFxListViewExtDownload

2. Anschließend erzeugst du das Grundgerüst der SPFx-Lösung mit dem Yeoman-Generator:

yo @microsoft/sharepoint

3. Der Generator stellt dir nun ein paar Fragen. Beantworte sie wie hier angegeben:

  • What is your solution name? Antwort: SPFxListViewExtDownload
  • Which type of client-side component to create? Antwort: Extension
  • Which type of client-side extension to create? Antwort: ListView Command Set
  • Add new Command Set to solution sp-fx-list-view-ext-download.
  • What is your Command Set name? Antwort: custDownload

So sehen die Antworten im Yeoman-Generator aus.

4. Sobald die Lösung erstellt ist, öffne die Datei package-solution.json im Ordner config und ergänze den folgenden Abschnitt:

"webApiPermissionRequests": [
  {
    "resource": "Microsoft Graph",
    "scope": "Mail.Send"
  }
]

Wichtig: Mit diesem Eintrag fordern wir die Berechtigung zum E-Mail-Versand an. Diese Berechtigung muss das Admin-Team im SharePoint Admin Center unter API access noch freigeben. Ohne diese Freigabe wird die E-Mail an den im Code hinterlegten Empfänger nicht verschickt.

Die Mail.Send-Berechtigung in der package-solution.json.

5. Öffne nun die Datei CustDownloadCommandSet.ts im Ordner src und ersetze den Standardcode komplett durch den folgenden Code. Trag oben bei EMAIL_RECIPIENT und RECIPIENT_NAME deine eigenen Werte ein:

import { Log } from '@microsoft/sp-core-library';
import {
  BaseListViewCommandSet,
  type Command,
  type IListViewCommandSetExecuteEventParameters,
  type ListViewStateChangedEventArgs
} from '@microsoft/sp-listview-extensibility';
import { MSGraphClientV3 } from '@microsoft/sp-http';

export interface ICustDownloadCommandSetProperties {
  // Beispiel, durch eigene Properties ersetzen
  sampleTextOne: string;
  sampleTextTwo: string;
}

const LOG_SOURCE: string = 'CustDownloadCommandSet';

// Hier deine eigenen Werte eintragen:
const EMAIL_RECIPIENT: string = 'empfaenger@deine-domain.de';
const RECIPIENT_NAME: string = 'Vorname des Empfaengers';

export default class CustDownloadCommandSet extends BaseListViewCommandSet<ICustDownloadCommandSetProperties> {

  public onInit(): Promise<void> {
    Log.info(LOG_SOURCE, 'Initialized CustDownloadCommandSet');

    // Startzustand der (ungenutzten) Standard-Buttons
    const compareOneCommand: Command = this.tryGetCommand('COMMAND_1');
    compareOneCommand.visible = false;
    this.context.listView.listViewStateChangedEvent.add(this, this._onListViewStateChanged);

    this._attachDownloadClickHandler();
    return Promise.resolve();
  }

  public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
    // Wir fuegen keine eigenen Buttons hinzu, daher passiert hier nichts.
    switch (event.itemId) {
      case 'COMMAND_1':
        break;
      case 'COMMAND_2':
        break;
      default:
        throw new Error('Unknown command');
    }
  }

  private _onListViewStateChanged = (args: ListViewStateChangedEventArgs): void => {
    Log.info(LOG_SOURCE, 'List view state changed');

    const compareOneCommand: Command = this.tryGetCommand('COMMAND_1');
    const compareTwoCommand: Command = this.tryGetCommand('COMMAND_2');
    if (compareOneCommand) {
      compareOneCommand.visible = false;
    }
    if (compareTwoCommand) {
      compareTwoCommand.visible = false;
    }

    this.raiseOnChange();
  }

  // Haengt einen Klick-Listener an den vorhandenen "Herunterladen"-Button.
  private _attachDownloadClickHandler(): void {
    const attachHandler = () => {
      const downloadButton = document.querySelector(
        'button[data-automationid="downloadCommand"]'
      ) as HTMLElement;

      if (downloadButton && !downloadButton.getAttribute('listener-attached')) {
        downloadButton.setAttribute('listener-attached', 'true');
        downloadButton.addEventListener('click', this._onDownloadClicked.bind(this));
        Log.info(LOG_SOURCE, 'Download button listener attached');
      }
    };

    // Erster Versuch direkt beim Start
    attachHandler();

    // Auf DOM-Aenderungen in Toolbar und Kontextmenue reagieren
    const observer = new MutationObserver(() => {
      attachHandler(); // erneut anhaengen, wenn SharePoint die Leiste neu rendert
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  // Laeuft, wenn der eingebaute Download-Button geklickt wird.
  private _onDownloadClicked(): void {
    try {
      const selectedRows = this.context.listView.selectedRows;
      if (selectedRows && selectedRows.length > 0) {
        const fileNames = selectedRows.map((r: any) => r.getValueByName('FileLeafRef'));
        const user = this.context.pageContext.user.displayName;

        this._sendDownloadEmail(fileNames, user);
      }
    } catch (err) {
      Log.error(LOG_SOURCE, err as Error);
    }
  }

  // Versendet per MS Graph eine E-Mail-Benachrichtigung beim Download.
  private _sendDownloadEmail(fileNames: string[], user: string): void {
    this.context.msGraphClientFactory
      .getClient('3')
      .then((client: MSGraphClientV3) => {
        const siteUrl = this.context.pageContext.web.absoluteUrl;
        const siteName = this.context.pageContext.web.title;

        const emailBody = `
          <div style="font-family:Segoe UI, Arial, sans-serif; font-size:14px; color:#333;">
            <p>Hallo <strong>${RECIPIENT_NAME}</strong>,</p>

            <p>diese Nachricht informiert dich darueber, dass <strong>${user}</strong>
            die folgende(n) Datei(en) von der SharePoint-Seite
            <a href="${siteUrl}" style="color:#0078D4; text-decoration:none;">${siteName}</a>
            heruntergeladen hat:</p>

            <ul style="margin-top:8px; margin-bottom:8px;">
              ${fileNames.map(f => `<li>${f}</li>`).join('')}
            </ul>

            <p style="margin-top:16px;">Viele Gruesse<br>
            <strong>SharePoint System</strong></p>
          </div>
        `;

        const email = {
          message: {
            subject: `Download-Benachrichtigung - ${user}`,
            body: {
              contentType: 'HTML',
              content: emailBody
            },
            toRecipients: [
              {
                emailAddress: { address: EMAIL_RECIPIENT }
              }
            ]
          },
          saveToSentItems: false
        };

        client.api('/me/sendMail').post(email)
          .then(() => Log.info(LOG_SOURCE, 'Email sent'))
          .catch(err => Log.error(LOG_SOURCE, err));
      })
      .catch(err => Log.error(LOG_SOURCE, err));
  }
}

Was der Code im Einzelnen macht

Damit du den Code nicht nur kopierst, sondern auch verstehst, hier die wichtigsten Stellen erklärt:

  • Ganz oben importieren wir MSGraphClientV3 aus @microsoft/sp-http. Das ist unser Werkzeug zum E-Mail-Versand.
  • EMAIL_RECIPIENT und RECIPIENT_NAME sind die beiden Variablen, die du anpassen musst. Die erste enthält die E-Mail-Adresse der Person, die benachrichtigt werden soll, die zweite ihren Namen für die Anrede.
  • _attachDownloadClickHandler() rufen wir in der onInit-Methode auf. Damit wird unser Handler direkt beim Start an den Standard-Button gehängt.
  • onExecute() ist der Standardcode, der bei einem Klick auf einen eigenen Button laufen würde. Da wir keine eigenen Buttons hinzufügen, passiert hier bewusst nichts.
  • _onListViewStateChanged() reagiert auf Änderungen in der Liste oder Bibliothek. Normalerweise steuert man hier die Sichtbarkeit eigener Buttons. Weil wir keine verwenden, sind beide auf false gesetzt. Sie tauchen also nirgends auf.

Die eigentliche Logik steckt in drei Funktionen:

  • _attachDownloadClickHandler() erkennt den von SharePoint gerenderten „Herunterladen”-Button über document.querySelector('button[data-automationid="downloadCommand"]'), hängt einen eigenen Event-Handler daran und sorgt mit einem MutationObserver dafür, dass der Handler erneut gesetzt wird, sobald SharePoint die Toolbar neu rendert. Das ist nötig, weil SharePoint die Befehlsleiste je nach Auswahl ständig neu aufbaut.
  • _onDownloadClicked() ist der eigene Handler, der beim Klick auf den Standard-Button anspringt. Er ermittelt, welche Dateien gerade ausgewählt sind, und stößt anschließend den E-Mail-Versand an.
  • _sendDownloadEmail(fileNames, user) bekommt die Dateinamen und den Namen des Nutzers übergeben. Über msGraphClientFactory.getClient('3') verbinden wir uns mit Microsoft Graph (Client-Version 3). Danach holen wir uns siteUrl und siteName aus dem Kontext, bauen den E-Mail-Text zusammen und verschicken die Nachricht über die Graph-API /me/sendMail.

Ein Wort zu /me/sendMail: Die Mail wird im Namen des angemeldeten Benutzers verschickt, also der Person, die gerade die Datei lädt. Sie taucht damit theoretisch in deren Postausgang auf. Wir setzen oben saveToSentItems: false, damit das nicht passiert. Wenn die Mail stattdessen von einem festen Systemkonto kommen soll, brauchst du einen anderen Aufbau mit Anwendungsberechtigungen und dem Endpunkt /users/{id}/sendMail. Das ist hier bewusst nicht abgedeckt.

Lokal testen

6. Um die Erweiterung lokal zu testen, öffne die Datei serve.json im Ordner config. Dort findest du den Eintrag pageUrl:

https://{tenantDomain}/SitePages/myPage.aspx

Ersetze diesen Wert durch die URL deiner eigenen SharePoint-Bibliothek, zum Beispiel:

"https://szg52.sharepoint.com/sites/SPFXDevelopment/HRDocuments/Forms/AllItems.aspx"

7. Danach startest du den lokalen Server, entweder in der Eingabeaufforderung oder im Terminal:

gulp serve

8. Klicke jetzt auf Load debug scripts, markiere ein paar Dateien in der Bibliothek und klicke auf „Herunterladen”. Kurz darauf trifft die E-Mail beim hinterlegten Empfänger ein.

Im Dialog auf „Load debug scripts" klicken, danach ist die Erweiterung in der Bibliothek aktiv.

Die Lösung produktiv bereitstellen

Mit gulp serve läuft die Erweiterung nur lokal zu Testzwecken. Wer sie dauerhaft in einer Bibliothek nutzen will, muss die Lösung paketieren und im App-Katalog bereitstellen. Das geht in vier Schritten.

1. Baue zuerst die optimierte Produktionsversion und lade die Dateien auf das Office-365-CDN hoch:

gulp bundle --ship

gulp package-solution --ship

Der zweite Befehl erzeugt im Ordner sharepoint/solution eine Datei mit der Endung .sppkg. Das ist das fertige Paket.

2. Öffne den App-Katalog deines Tenants. Die URL hat üblicherweise die Form https://{deinTenant}.sharepoint.com/sites/appcatalog. Lade die .sppkg-Datei dort per Drag-and-drop hoch. SharePoint fragt anschließend, ob die Lösung allen Seiten zur Verfügung stehen soll. Bestätige das mit Bereitstellen.

3. Jetzt kommt der Schritt, den man leicht vergisst: Die unter Punkt 4 angeforderte Mail.Send-Berechtigung muss freigegeben werden. Wechsle dafür ins SharePoint Admin Center, öffne Erweiterte Funktionen und dann API-Zugriff. Dort liegt eine ausstehende Anfrage für Microsoft Graph mit dem Bereich Mail.Send. Genehmige sie. Ohne diese Freigabe wird keine E-Mail verschickt.

4. Füge die App zur gewünschten SharePoint-Seite hinzu. Gehe dazu auf der Zielseite unter Einstellungen auf Apps hinzufügen und wähle deine Lösung aus. Sobald die App installiert ist, wird die Erweiterung in den Bibliotheken dieser Seite aktiv. Ein Reiter über „Load debug scripts” ist dann nicht mehr nötig.

Damit läuft die Erweiterung dauerhaft und für alle berechtigten Nutzer der Seite.

Grenzen dieses Ansatzes

Der gezeigte Weg funktioniert, hat aber ein paar Eigenheiten, die du kennen solltest, bevor du dich darauf verlässt.

Wir hängen uns an einen internen SharePoint-Button. Das Attribut data-automationid="downloadCommand" ist nicht Teil einer offiziellen, von Microsoft zugesicherten Schnittstelle. Microsoft kann die Befehlsleiste jederzeit umbauen. Wenn sich dieses Attribut ändert, greift unser querySelector ins Leere und die Benachrichtigung bleibt still aus, ohne Fehlermeldung. Plane also ein, das nach größeren SharePoint-Updates kurz zu prüfen.

Der Handler reagiert auf den Klick, nicht auf den abgeschlossenen Download. Bricht ein Nutzer den Download im Browser-Dialog ab, geht die E-Mail trotzdem raus. Für eine grobe Nachvollziehbarkeit reicht das meistens, ein lückenloses Download-Protokoll ist es aber nicht.

Der MutationObserver beobachtet den gesamten Seiteninhalt. Auf großen, aktiven Seiten kostet das etwas Leistung. In der Praxis fällt das kaum auf, sauberer wäre es trotzdem, den beobachteten Bereich auf die Toolbar einzugrenzen, sobald sie im DOM steht.

Wer es robuster braucht, sollte sich serverseitige Alternativen ansehen. Ein guter Kandidat ist eine Power Automate Flow auf Basis der SharePoint-Audit-Logs. Die ist unabhängig vom Aufbau der Oberfläche, dafür aber träger und aufwendiger einzurichten.

Datenschutz nicht vergessen

Diese Lösung erfasst, welche Person welche Datei wann lädt, und gibt diese Information an Dritte weiter. Das ist eine Verarbeitung personenbezogener Daten und fällt damit unter die DSGVO. Bevor du das produktiv einsetzt, klär das mit den richtigen Stellen ab: betroffene Personen müssen informiert werden, bei Beschäftigten ist je nach Land und Betrieb die Arbeitnehmervertretung einzubeziehen, und es braucht einen sachlichen Zweck für die Protokollierung. Im Zweifel hältst du Rücksprache mit dem Datenschutzbeauftragten. Technisch machbar heißt nicht automatisch zulässig.

Mein Fazit

Du hast jetzt gesehen, wie man die Standard-Buttons in der Befehlsleiste und im Kontextmenü einer SharePoint-Liste oder -Bibliothek anspricht und mit dem SharePoint Framework um eigene Logik erweitert. Der Ansatz ist pragmatisch und schnell umgesetzt, lebt aber davon, dass sich SharePoint intern nicht zu stark verändert. Für unkritische Szenarien ist das in Ordnung, für ein rechtssicheres Protokoll würde ich eher zur serverseitigen Variante greifen.

Mein Tipp: Bau das Beispiel einmal selbst nach. Schreib mir gern über das Kontaktformular, wenn du Fragen zu diesem Thema hast oder irgendwo hängen bleibst.